diff --git a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs deleted file mode 100644 index 84f1fc873..000000000 --- a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs +++ /dev/null @@ -1,362 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Cache; -using NzbDrone.Core.Applications; -using NzbDrone.Core.Applications.Listenarr; -using NzbDrone.Core.Configuration; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Indexers; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Test.Framework; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Applications.Listenarr -{ - [TestFixture] - public class ListenarrFixture : CoreTest - { - [SetUp] - public void Setup() - { - Subject.Definition = new ApplicationDefinition - { - Settings = new ListenarrSettings - { - ProwlarrUrl = "http://localhost:9696", - BaseUrl = "http://localhost:4545", - ApiKey = "abc", - SyncCategories = new List { NewznabStandardCategory.AudioAudiobook.Id } - } - }; - - Mocker.GetMock().SetupGet(c => c.ApiKey).Returns("abc"); - - var cached = new Mock>>(); - cached.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - - Mocker.GetMock().Setup(m => m.GetCache>(It.IsAny())).Returns(cached.Object); - } - - [Test] - public void GetIndexerMappings_should_return_mappings_when_baseUrl_matches_prowlarr() - { - var indexer = new ListenarrIndexer - { - Id = 99, - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = "http://localhost:9696/45/api" }, - new ListenarrField { Name = "apiKey", Value = "abc" } - } - }; - - Mocker.GetMock().Setup(c => c.GetIndexers(It.IsAny())).Returns(new List { indexer }); - - var mappings = Subject.GetIndexerMappings(); - - mappings.Should().HaveCount(1); - mappings[0].IndexerId.Should().Be(45); - mappings[0].RemoteIndexerId.Should().Be(99); - } - - [Test] - public void GetIndexerMappings_should_skip_non_matching_api_key_and_baseurl() - { - var indexer = new ListenarrIndexer - { - Id = 100, - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = "http://external/1/api" }, - new ListenarrField { Name = "apiKey", Value = "wrong" } - } - }; - - Mocker.GetMock().Setup(c => c.GetIndexers(It.IsAny())).Returns(new List { indexer }); - - var mappings = Subject.GetIndexerMappings(); - - mappings.Should().BeEmpty(); - } - - [Test] - public void Test_should_call_testconnection_and_return_success_when_valid() - { - var schema = new List - { - new ListenarrIndexer - { - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = "" }, - new ListenarrField { Name = "apiPath", Value = "" }, - new ListenarrField { Name = "apiKey", Value = "" }, - new ListenarrField { Name = "categories", Value = new List() } - } - } - }; - - Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); - Mocker.GetMock().Setup(c => c.TestConnection(It.IsAny(), It.IsAny())).Returns((FluentValidation.Results.ValidationFailure)null).Verifiable(); - - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); - - var result = Subject.Test(); - - result.IsValid.Should().BeTrue(); - Mocker.GetMock().Verify(m => m.TestConnection(It.IsAny(), It.IsAny()), Times.Once); - } - - [Test] - public void Test_should_retry_and_use_fresh_schema_when_cached_schema_is_incomplete() - { - var cachedSchema = new List - { - new ListenarrIndexer - { - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "apiKey", Value = "" } - } - } - }; - - var freshSchema = new List - { - new ListenarrIndexer - { - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = "" }, - new ListenarrField { Name = "apiPath", Value = "" }, - new ListenarrField { Name = "apiKey", Value = "" }, - new ListenarrField { Name = "categories", Value = new List() } - } - } - }; - - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => cachedSchema); - - typeof(Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); - - Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(freshSchema); - Mocker.GetMock().Setup(c => c.TestConnection(It.IsAny(), It.IsAny())).Returns((FluentValidation.Results.ValidationFailure)null); - - var result = Subject.Test(); - - result.IsValid.Should().BeTrue(); - Mocker.GetMock().Verify(m => m.GetIndexerSchema(It.IsAny()), Times.AtLeastOnce); - } - - [Test] - public void Test_should_handle_exception_from_testconnection() - { - var schema = new List - { - new ListenarrIndexer - { - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = "" }, - new ListenarrField { Name = "apiPath", Value = "" }, - new ListenarrField { Name = "apiKey", Value = "" }, - new ListenarrField { Name = "categories", Value = new List() } - } - } - }; - - Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); - Mocker.GetMock().Setup(c => c.TestConnection(It.IsAny(), It.IsAny())).Throws(new Exception("boom")); - - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); - - var result = Subject.Test(); - - result.IsValid.Should().BeFalse(); - result.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("Unable to complete application test")); - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void Test_should_fail_when_schema_missing() - { - Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(new List()); - - var method = typeof(Core.Applications.Listenarr.Listenarr).GetMethod("BuildListenarrIndexer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var indexerDef = new IndexerDefinition { Id = 1, Name = "Test", Protocol = DownloadProtocol.Usenet, Capabilities = new IndexerCapabilities() }; - - var ex = Assert.Throws(() => method.Invoke(Subject, new object[] { indexerDef, indexerDef.Capabilities, DownloadProtocol.Usenet, 0 })); - Assert.IsInstanceOf(ex.InnerException); - Assert.That(ex.InnerException.Message, Does.Contain("indexer schemas")); - ExceptionVerification.ExpectedWarns(1); - } - - [Test] - public void Test_should_fail_when_schema_missing_required_fields() - { - var schema = new List - { - new ListenarrIndexer - { - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "apiKey", Value = "" } - } - } - }; - - Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); - - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); - - try - { - var result = Subject.Test(); - result.IsValid.Should().BeFalse(); - result.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("missing required fields")); - } - finally - { - ExceptionVerification.IgnoreWarns(); - } - } - - [Test] - public void AddIndexer_should_insert_app_indexer_mapping_on_success() - { - var indexerDefinition = new IndexerDefinition - { - Id = 12, - Name = "TestIndexer", - Protocol = DownloadProtocol.Usenet, - Capabilities = new IndexerCapabilities(), - Enable = true, - AppProfile = new LazyLoaded(new AppSyncProfile { EnableRss = true, EnableAutomaticSearch = true, EnableInteractiveSearch = true }) - }; - - indexerDefinition.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioAudiobook); - - var mockIndexer = new Mock(); - mockIndexer.Setup(i => i.GetCapabilities()).Returns(indexerDefinition.Capabilities); - - Mocker.GetMock().Setup(m => m.GetInstance(It.IsAny())).Returns(mockIndexer.Object); - - var schema = new List - { - new ListenarrIndexer - { - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = "" }, - new ListenarrField { Name = "apiPath", Value = "" }, - new ListenarrField { Name = "apiKey", Value = "" }, - new ListenarrField { Name = "categories", Value = new List() } - } - } - }; - - Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); - Mocker.GetMock().Setup(c => c.AddIndexer(It.IsAny(), It.IsAny())).Returns(new ListenarrIndexer { Id = 501 }); - - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); - - indexerDefinition.Capabilities.Categories.SupportedCategories(((ListenarrSettings)Subject.Definition.Settings).SyncCategories.ToArray()).Should().NotBeEmpty(); - - Subject.AddIndexer(indexerDefinition); - - Mocker.GetMock().Verify(m => m.AddIndexer(It.IsAny(), It.IsAny()), Times.Once()); - Mocker.GetMock().Verify(m => m.Insert(It.Is(a => a.IndexerId == 12 && a.RemoteIndexerId == 501)), Times.Once()); - } - - [Test] - public void AddIndexer_should_use_existing_remote_indexer_if_baseUrl_matches() - { - var indexerDefinition = new IndexerDefinition - { - Id = 12, - Name = "TestIndexer", - Protocol = DownloadProtocol.Usenet, - Capabilities = new IndexerCapabilities(), - Enable = true, - AppProfile = new LazyLoaded(new AppSyncProfile { EnableRss = true, EnableAutomaticSearch = true, EnableInteractiveSearch = true }) - }; - - indexerDefinition.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioAudiobook); - - var mockIndexer = new Mock(); - mockIndexer.Setup(i => i.GetCapabilities()).Returns(indexerDefinition.Capabilities); - - Mocker.GetMock().Setup(m => m.GetInstance(It.IsAny())).Returns(mockIndexer.Object); - - var schema = new List - { - new ListenarrIndexer - { - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = "" }, - new ListenarrField { Name = "apiPath", Value = "" }, - new ListenarrField { Name = "apiKey", Value = "" }, - new ListenarrField { Name = "categories", Value = new List() } - } - } - }; - - var existing = new ListenarrIndexer - { - Id = 501, - Implementation = "Newznab", - Fields = new List - { - new ListenarrField { Name = "baseUrl", Value = $"{((ListenarrSettings)Subject.Definition.Settings).ProwlarrUrl.TrimEnd('/')}/12/" }, - new ListenarrField { Name = "apiPath", Value = "/api" }, - new ListenarrField { Name = "apiKey", Value = "abc" }, - new ListenarrField { Name = "categories", Value = new List() } - } - }; - - Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); - Mocker.GetMock().Setup(c => c.GetIndexers(It.IsAny())).Returns(new List { existing }); - - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); - - indexerDefinition.Capabilities.Categories.SupportedCategories(((ListenarrSettings)Subject.Definition.Settings).SyncCategories.ToArray()).Should().NotBeEmpty(); - - Subject.AddIndexer(indexerDefinition); - - Mocker.GetMock().Verify(m => m.AddIndexer(It.IsAny(), It.IsAny()), Times.Never()); - Mocker.GetMock().Verify(m => m.Insert(It.Is(a => a.IndexerId == 12 && a.RemoteIndexerId == 501)), Times.Once()); - } - } -} diff --git a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs deleted file mode 100644 index 5500b696a..000000000 --- a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System; -using System.Net; -using FluentAssertions; -using Moq; -using NUnit.Framework; -using NzbDrone.Common.Http; -using NzbDrone.Common.Serializer; -using NzbDrone.Core.Applications.Listenarr; -using NzbDrone.Test.Common; - -namespace NzbDrone.Core.Test.Applications.Listenarr -{ - [TestFixture] - public class ListenarrV1ProxyFixture : TestBase - { - [Test] - public void GetIndexers_should_deserialize_json_and_set_api_key_header() - { - var settings = new ListenarrSettings { BaseUrl = "http://localhost:4545", ApiKey = "abc123" }; - - var responseJson = new[] - { - new - { - Id = "42", - Name = "Test", - Implementation = "Newznab", - Fields = new[] - { - new { name = "baseUrl", value = "http://localhost:4545/1/api" }, - new { name = "apiKey", value = "x" }, - } - } - }.ToJson(); - - HttpRequest capturedRequest = null; - - Mocker.GetMock() - .Setup(c => c.Execute(It.IsAny())) - .Returns(req => - { - capturedRequest = req; - return new HttpResponse(req, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), System.Text.Encoding.UTF8.GetBytes(responseJson), 0, HttpStatusCode.OK, new Version("1.0")); - }); - - var result = Subject.GetIndexers(settings); - - result.Should().NotBeNull(); - result.Count.Should().Be(1); - capturedRequest.Headers.GetSingleValue("X-Api-Key").Should().Be("abc123"); - - (capturedRequest.Url.ToString().Contains("/api/indexer") || capturedRequest.Url.ToString().Contains("/api/v1/indexer")).Should().BeTrue(); - } - - [Test] - public void GetIndexerSchema_should_handle_single_object_response_with_fields_object() - { - var settings = new ListenarrSettings { BaseUrl = "http://localhost:4545", ApiKey = "abc123" }; - - var json = "[ { \"id\": 1, \"implementation\": \"Newznab\", \"fields\": [ { \"name\": \"baseUrl\", \"type\": \"text\" }, { \"name\": \"apiKey\", \"type\": \"text\" } ] } ]"; - - Mocker.GetMock() - .Setup(c => c.Execute(It.IsAny())) - .Returns(req => new HttpResponse(req, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), json, 0, HttpStatusCode.OK, new Version("1.0"))); - - var result = Subject.GetIndexerSchema(settings); - - result.Should().NotBeNull(); - result.Count.Should().Be(1); - result[0].Fields.Should().NotBeNull(); - result[0].Fields.Count.Should().Be(2); - result[0].Fields.Should().Contain(f => f.Name == "baseUrl"); - result[0].Fields.Should().Contain(f => f.Name == "apiKey"); - } - - [Test] - public void GetIndexerSchema_should_preserve_implementations_array_as_list() - { - var settings = new ListenarrSettings { BaseUrl = "http://localhost:4545", ApiKey = "abc123" }; - - var json = "[ { \"fields\": [ { \"name\": \"baseUrl\", \"type\": \"text\" } ], \"implementations\": [\"Newznab\",\"Torznab\"] } ]"; - - Mocker.GetMock() - .Setup(c => c.Execute(It.IsAny())) - .Returns(req => new HttpResponse(req, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), json, 0, HttpStatusCode.OK, new Version("1.0"))); - - var result = Subject.GetIndexerSchema(settings); - - result.Should().NotBeNull(); - result.Count.Should().Be(1); - result[0].Implementations.Should().NotBeNull(); - result[0].Implementations.Should().Contain("Newznab"); - result[0].Implementations.Should().Contain("Torznab"); - } - - [Test] - public void Execute_should_throw_application_exception_when_unauthorized() - { - var settings = new ListenarrSettings { BaseUrl = "http://localhost:4545", ApiKey = "bad" }; - - Mocker.GetMock() - .Setup(c => c.Execute(It.IsAny())) - .Throws(new HttpException(new HttpResponse(new HttpRequest("http://localhost/"), new HttpHeader(), new CookieCollection(), "unauthorized", 0, HttpStatusCode.Unauthorized, new Version("1.0")))); - - Assert.Throws(() => Subject.GetIndexers(settings)); - - ExceptionVerification.ExpectedWarns(0); - } - } -} diff --git a/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs b/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs index b7579983c..6b13f67c9 100644 --- a/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs +++ b/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs @@ -90,17 +90,17 @@ public override ValidationResult Test() public override List GetIndexerMappings() { - var indexers = (_listenarrV1Proxy.GetIndexers(Settings) ?? new List()) + var indexers = _listenarrV1Proxy.GetIndexers(Settings) .Where(i => i.Implementation is "Newznab" or "Torznab"); var mappings = new List(); foreach (var indexer in indexers) { - var baseUrl = (string)indexer.Fields?.FirstOrDefault(x => x.Name == "baseUrl")?.Value ?? string.Empty; + var baseUrl = (string)indexer.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value ?? string.Empty; if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) && - (string)indexer.Fields?.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey) + (string)indexer.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey) { continue; } @@ -109,6 +109,7 @@ public override List GetIndexerMappings() if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId)) { + // Add parsed mapping if it's mapped to a Indexer in this Prowlarr instance mappings.Add(new AppIndexerMap { IndexerId = indexerId, RemoteIndexerId = indexer.Id }); } } @@ -138,27 +139,6 @@ public override void AddIndexer(IndexerDefinition indexer) var listenarrIndexer = BuildListenarrIndexer(indexer, indexerCapabilities, indexer.Protocol); - try - { - var remoteIndexers = _listenarrV1Proxy.GetIndexers(Settings); - var baseUrl = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; - - var match = remoteIndexers.FirstOrDefault(r => - ((string)r.Fields.FirstOrDefault(f => f.Name == "baseUrl")?.Value ?? "").Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase) - || (r.Name?.Contains(indexer.Name, StringComparison.InvariantCultureIgnoreCase) == true)); - - if (match != null) - { - _logger.Debug("Found existing remote indexer for {0} as {1} [{2}], inserting mapping", indexer.Name, match.Name, match.Id); - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = match.Id }); - return; - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Error while attempting to discover existing remote indexer for {0} [{1}]", indexer.Name, indexer.Id); - } - var remoteIndexer = _listenarrV1Proxy.AddIndexer(listenarrIndexer, Settings); if (remoteIndexer == null) @@ -193,126 +173,7 @@ public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = f var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id); - if (indexerMapping == null) - { - if ((indexerCapabilities.MusicSearchAvailable || indexerCapabilities.SearchAvailable) && - indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) - { - _logger.Debug("No mapping found for {0} [{1}], adding to Listenarr", indexer.Name, indexer.Id); - - var listenarrIndexerToAdd = BuildListenarrIndexer(indexer, indexerCapabilities, indexer.Protocol); - listenarrIndexerToAdd.Id = 0; - ListenarrIndexer newRemoteIndexer = null; - - try - { - newRemoteIndexer = _listenarrV1Proxy.AddIndexer(listenarrIndexerToAdd, Settings); - } - catch (HttpException ex) - { - _logger.Warn(ex, "Failed to add indexer {0} [{1}] to Listenarr: {2}", indexer.Name, indexer.Id, ex.Response?.StatusCode); - } - - if (newRemoteIndexer != null) - { - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = newRemoteIndexer.Id }); - } - else - { - try - { - var remoteIndexers = _listenarrV1Proxy.GetIndexers(Settings); - var baseUrl = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; - - var match = remoteIndexers.FirstOrDefault(r => - ((string)r.Fields.FirstOrDefault(f => f.Name == "baseUrl")?.Value ?? "").Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase) - || (r.Name?.Contains(indexer.Name, StringComparison.InvariantCultureIgnoreCase) == true)); - - if (match != null) - { - _logger.Debug("Found existing remote indexer for {0} as {1} [{2}], inserting mapping", indexer.Name, match.Name, match.Id); - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = match.Id }); - } - else - { - _logger.Debug("No remote indexer found for {0} after failing to add; skipping mapping", indexer.Name); - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Error while attempting to discover existing remote indexer for {0} [{1}]", indexer.Name, indexer.Id); - } - } - } - else - { - _logger.Debug("No mapping found for {0} [{1}], skipping add due to indexer capabilities", indexer.Name, indexer.Id); - } - - return; - } - - if (indexerMapping.RemoteIndexerId == 0) - { - _logger.Warn("Mapping for indexer {0} contains invalid remote id 0, removing mapping and re-adding if possible", indexer.Id); - _appIndexerMapService.Delete(indexerMapping.Id); - - if ((indexerCapabilities.MusicSearchAvailable || indexerCapabilities.SearchAvailable) && - indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) - { - var listenarrIndexerToAdd = BuildListenarrIndexer(indexer, indexerCapabilities, indexer.Protocol); - listenarrIndexerToAdd.Id = 0; - ListenarrIndexer newRemoteIndexer = null; - - try - { - newRemoteIndexer = _listenarrV1Proxy.AddIndexer(listenarrIndexerToAdd, Settings); - } - catch (HttpException ex) - { - _logger.Warn(ex, "Failed to add indexer {0} [{1}] to Listenarr: {2}", indexer.Name, indexer.Id, ex.Response?.StatusCode); - } - - if (newRemoteIndexer != null) - { - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = newRemoteIndexer.Id }); - } - else - { - try - { - var remoteIndexers = _listenarrV1Proxy.GetIndexers(Settings); - var baseUrl = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; - - var match = remoteIndexers.FirstOrDefault(r => - ((string)r.Fields.FirstOrDefault(f => f.Name == "baseUrl")?.Value ?? "").Equals(baseUrl, StringComparison.InvariantCultureIgnoreCase) - || (r.Name?.Contains(indexer.Name, StringComparison.InvariantCultureIgnoreCase) == true)); - - if (match != null) - { - _logger.Debug("Found existing remote indexer for {0} as {1} [{2}], inserting mapping", indexer.Name, match.Name, match.Id); - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = match.Id }); - } - else - { - _logger.Debug("No remote indexer found for {0} after failing to add; skipping mapping", indexer.Name); - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Error while attempting to discover existing remote indexer for {0} [{1}]", indexer.Name, indexer.Id); - } - } - } - else - { - _logger.Debug("Skipping re-add due to indexer capabilities for {0} [{1}]", indexer.Name, indexer.Id); - } - - return; - } - - var listenarrIndexer = BuildListenarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping.RemoteIndexerId); + var listenarrIndexer = BuildListenarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0); var remoteIndexer = _listenarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings); @@ -327,24 +188,21 @@ public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = f if ((indexerCapabilities.MusicSearchAvailable || indexerCapabilities.SearchAvailable) && indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any()) { - if (remoteIndexer.Fields != null) - { - listenarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => listenarrIndexer.Fields.All(s => s.Name != f.Name))); - } + // Retain user fields not-affiliated with Prowlarr + listenarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => listenarrIndexer.Fields.All(s => s.Name != f.Name))); - if (remoteIndexer.Tags != null) - { - listenarrIndexer.Tags.UnionWith(remoteIndexer.Tags); - } + // Retain user tags not-affiliated with Prowlarr + listenarrIndexer.Tags.UnionWith(remoteIndexer.Tags); + // Retain user settings not-affiliated with Prowlarr listenarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId; - listenarrIndexer.Id = remoteIndexer.Id; - + // Update the indexer if it still has categories that match _listenarrV1Proxy.UpdateIndexer(listenarrIndexer, Settings); } else { + // Else remove it, it no longer should be used _listenarrV1Proxy.RemoveIndexer(remoteIndexer.Id, Settings); _appIndexerMapService.Delete(indexerMapping.Id); } @@ -360,11 +218,11 @@ public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = f _logger.Debug("Remote indexer not found, re-adding {0} [{1}] to Listenarr", indexer.Name, indexer.Id); listenarrIndexer.Id = 0; var newRemoteIndexer = _listenarrV1Proxy.AddIndexer(listenarrIndexer, Settings); - - if (newRemoteIndexer != null) - { - _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = newRemoteIndexer.Id }); - } + _appIndexerMapService.Insert(new AppIndexerMap { AppId = Definition.Id, IndexerId = indexer.Id, RemoteIndexerId = newRemoteIndexer.Id }); + } + else + { + _logger.Debug("Remote indexer not found for {0} [{1}], skipping re-add to Listenarr due to indexer capabilities", indexer.Name, indexer.Id); } } } @@ -373,56 +231,20 @@ private ListenarrIndexer BuildListenarrIndexer(IndexerDefinition indexer, Indexe { var cacheKey = $"{Settings.BaseUrl}"; var schemas = _schemaCache.Get(cacheKey, () => _listenarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7)); - var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime", "seedCriteria.discographySeedTime", "rejectBlocklistedTorrentHashesWhileGrabbing" }; + var syncFields = new List { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" }; - if (schemas == null || !schemas.Any()) - { - try - { - schemas = _listenarrV1Proxy.GetIndexerSchema(Settings); - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to fetch indexer schemas from Listenarr at {0}", Settings.BaseUrl); - schemas = null; - } - } + var newznab = schemas.First(i => i.Implementation == "Newznab"); + var torznab = schemas.First(i => i.Implementation == "Torznab"); - if (schemas == null || !schemas.Any()) - { - _logger.Warn("No indexer schemas were returned from Listenarr at {0}", Settings.BaseUrl); - throw new ApplicationException("Listenarr returned no indexer schemas. Ensure Listenarr exposes '/api/v1/indexer/schema' and that it returns a JSON array of schemas containing 'Newznab' or 'Torznab'."); - } - - if (id == 0) - { - syncFields.AddRange(new List { "additionalParameters" }); - } - - var newznab = schemas.FirstOrDefault(i => string.Equals(i.Implementation, "Newznab", StringComparison.InvariantCultureIgnoreCase) || (i.Implementations != null && i.Implementations.Any(s => string.Equals(s, "Newznab", StringComparison.InvariantCultureIgnoreCase)))); - var torznab = schemas.FirstOrDefault(i => string.Equals(i.Implementation, "Torznab", StringComparison.InvariantCultureIgnoreCase) || (i.Implementations != null && i.Implementations.Any(s => string.Equals(s, "Torznab", StringComparison.InvariantCultureIgnoreCase)))); - - if (newznab == null && torznab == null) - { - _logger.Warn("Indexer schemas are missing 'Newznab' and 'Torznab' implementations from Listenarr at {0}", Settings.BaseUrl); - throw new ApplicationException("Listenarr indexer schema must include at least one of 'Newznab' or 'Torznab' implementations."); - } - - var schema = protocol == DownloadProtocol.Usenet ? newznab ?? torznab : torznab ?? newznab; - - if (schema == null) - { - _logger.Warn("No schema available for protocol {0} from Listenarr at {1}", protocol, Settings.BaseUrl); - throw new ApplicationException($"Listenarr indexer schema does not contain a suitable implementation for protocol {protocol}."); - } + var schema = protocol == DownloadProtocol.Usenet ? newznab : torznab; var listenarrIndexer = new ListenarrIndexer { Id = id, Name = $"{indexer.Name} (Prowlarr)", - EnableRss = indexer.Enable && (indexer.AppProfile?.Value?.EnableRss ?? false), - EnableAutomaticSearch = indexer.Enable && (indexer.AppProfile?.Value?.EnableAutomaticSearch ?? false), - EnableInteractiveSearch = indexer.Enable && (indexer.AppProfile?.Value?.EnableInteractiveSearch ?? false), + EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss, + EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch, + EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch, Priority = indexer.Priority, Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", ConfigContract = schema.ConfigContract, @@ -432,71 +254,10 @@ private ListenarrIndexer BuildListenarrIndexer(IndexerDefinition indexer, Indexe listenarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name))); - var requiredFieldNames = new List { "baseUrl", "apiPath", "apiKey", "categories" }; - var missing = requiredFieldNames.Where(f => listenarrIndexer.Fields.All(x => x.Name != f)).ToList(); - - if (missing.Any()) - { - _logger.Debug("Cached schema is missing required fields [{0}]. Attempting to refresh schema from proxy", string.Join(", ", missing)); - - try - { - var freshSchemas = _listenarrV1Proxy.GetIndexerSchema(Settings); - - if (freshSchemas != null && freshSchemas.Any()) - { - var freshNewznab = freshSchemas.FirstOrDefault(i => string.Equals(i.Implementation, "Newznab", StringComparison.InvariantCultureIgnoreCase) || (i.Implementations != null && i.Implementations.Any(s => string.Equals(s, "Newznab", StringComparison.InvariantCultureIgnoreCase)))); - var freshTorznab = freshSchemas.FirstOrDefault(i => string.Equals(i.Implementation, "Torznab", StringComparison.InvariantCultureIgnoreCase) || (i.Implementations != null && i.Implementations.Any(s => string.Equals(s, "Torznab", StringComparison.InvariantCultureIgnoreCase)))); - - var freshSchema = protocol == DownloadProtocol.Usenet ? freshNewznab ?? freshTorznab : freshTorznab ?? freshNewznab; - - if (freshSchema != null) - { - listenarrIndexer.Fields = freshSchema.Fields.Where(x => syncFields.Contains(x.Name)).ToList(); - missing = requiredFieldNames.Where(f => listenarrIndexer.Fields.All(x => x.Name != f)).ToList(); - - if (!missing.Any()) - { - _logger.Debug("Fresh schema contained required fields; proceeding with fresh schema"); - } - } - } - } - catch (Exception ex) - { - _logger.Warn(ex, "Unable to refresh indexer schemas from Listenarr at {0}", Settings.BaseUrl); - } - } - - if (missing.Any()) - { - _logger.Warn("Indexer schema missing required fields [{0}] from Listenarr at {1}", string.Join(", ", missing), Settings.BaseUrl); - throw new ApplicationException($"Listenarr indexer schema missing required fields: {string.Join(", ", missing)}. Ensure '/api/v1/indexer/schema' includes these fields."); - } - - var field = listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl"); - if (field != null) - { - field.Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; - } - - field = listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath"); - if (field != null) - { - field.Value = "/api"; - } - - field = listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey"); - if (field != null) - { - field.Value = _configFileProvider.ApiKey; - } - - field = listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories"); - if (field != null) - { - field.Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); - } + listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value = $"{Settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/"; + listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiPath").Value = "/api"; + listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "apiKey").Value = _configFileProvider.ApiKey; + listenarrIndexer.Fields.FirstOrDefault(x => x.Name == "categories").Value = JArray.FromObject(indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray())); return listenarrIndexer; } diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs index 06fafb09f..11b3a2b9f 100644 --- a/src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs +++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrIndexer.cs @@ -15,12 +15,12 @@ public class ListenarrIndexer public string Name { get; set; } public string ImplementationName { get; set; } public string Implementation { get; set; } - public List Implementations { get; set; } public string ConfigContract { get; set; } public string InfoLink { get; set; } public int? DownloadClientId { get; set; } public HashSet Tags { get; set; } public List Fields { get; set; } + public bool Equals(ListenarrIndexer other) { if (ReferenceEquals(null, other)) @@ -28,28 +28,37 @@ public bool Equals(ListenarrIndexer other) return false; } - // baseUrl comparison (case-insensitive) - var baseUrlEqual = string.Equals( - (string)Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value, - (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl")?.Value, - StringComparison.InvariantCultureIgnoreCase); + var baseUrl = (string)Fields.FirstOrDefault(x => x.Name == "baseUrl").Value == (string)other.Fields.FirstOrDefault(x => x.Name == "baseUrl").Value; + var cats = JToken.DeepEquals((JArray)Fields.FirstOrDefault(x => x.Name == "categories").Value, (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories").Value); - // categories deep equality - var catsEqual = JToken.DeepEquals( - (JArray)Fields.FirstOrDefault(x => x.Name == "categories")?.Value, - (JArray)other.Fields.FirstOrDefault(x => x.Name == "categories")?.Value); - - // apiKey: treat masked remote key as equal var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value; - var apiKeyEqual = apiKey == otherApiKey || otherApiKey == "********"; + var apiKeyCompare = apiKey == otherApiKey || otherApiKey == "********"; - // apiPath compare (could be null) - var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value; - var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value; - var apiPathEqual = Equals(apiPath, otherApiPath); + var apiPath = Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : Fields.FirstOrDefault(x => x.Name == "apiPath").Value; + var otherApiPath = other.Fields.FirstOrDefault(x => x.Name == "apiPath")?.Value == null ? null : other.Fields.FirstOrDefault(x => x.Name == "apiPath").Value; + var apiPathCompare = apiPath.Equals(otherApiPath); - return apiKeyEqual && apiPathEqual && baseUrlEqual && catsEqual; + var minimumSeeders = Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); + var otherMinimumSeeders = other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "minimumSeeders").Value); + var minimumSeedersCompare = minimumSeeders == otherMinimumSeeders; + + var seedTime = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value); + var otherSeedTime = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime")?.Value == null ? null : (int?)Convert.ToInt32(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedTime").Value); + var seedTimeCompare = seedTime == otherSeedTime; + + var seedRatio = Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); + var otherSeedRatio = other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio")?.Value == null ? null : (double?)Convert.ToDouble(other.Fields.FirstOrDefault(x => x.Name == "seedCriteria.seedRatio").Value); + var seedRatioCompare = seedRatio == otherSeedRatio; + + return other.EnableRss == EnableRss && + other.EnableAutomaticSearch == EnableAutomaticSearch && + other.EnableInteractiveSearch == EnableInteractiveSearch && + other.Name == Name && + other.Implementation == Implementation && + other.Priority == Priority && + other.Id == Id && + apiKeyCompare && apiPathCompare && baseUrl && cats && minimumSeedersCompare && seedRatioCompare && seedTimeCompare; } } }