diff --git a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs index 5f5f0fccd..3dba5e6bb 100644 --- a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs +++ b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs @@ -17,7 +17,7 @@ namespace NzbDrone.Core.Test.Applications.Listenarr { [TestFixture] - public class ListenarrFixture : CoreTest + public class ListenarrFixture : CoreTest { [SetUp] public void Setup() @@ -37,17 +37,16 @@ public void Setup() // Ensure cache calls execute factory each time during tests to avoid stale cached values // Use ICached> mock via dynamic mocking to avoid depending on concrete implementation - var cached = new Mock>>(); - cached.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); + 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); + Mocker.GetMock().Setup(m => m.GetCache>(It.IsAny())).Returns(cached.Object); } [Test] public void GetIndexerMappings_should_return_mappings_when_baseUrl_matches_prowlarr() { - // Arrange var indexer = new ListenarrIndexer { Id = 99, @@ -61,10 +60,8 @@ public void GetIndexerMappings_should_return_mappings_when_baseUrl_matches_prowl Mocker.GetMock().Setup(c => c.GetIndexers(It.IsAny())).Returns(new List { indexer }); - // Act var mappings = Subject.GetIndexerMappings(); - // Assert mappings.Should().HaveCount(1); mappings[0].IndexerId.Should().Be(45); mappings[0].RemoteIndexerId.Should().Be(99); @@ -73,7 +70,6 @@ public void GetIndexerMappings_should_return_mappings_when_baseUrl_matches_prowl [Test] public void GetIndexerMappings_should_skip_non_matching_api_key_and_baseurl() { - // Arrange var indexer = new ListenarrIndexer { Id = 100, @@ -87,17 +83,14 @@ public void GetIndexerMappings_should_skip_non_matching_api_key_and_baseurl() Mocker.GetMock().Setup(c => c.GetIndexers(It.IsAny())).Returns(new List { indexer }); - // Act var mappings = Subject.GetIndexerMappings(); - // Assert mappings.Should().BeEmpty(); } [Test] public void Test_should_call_testconnection_and_return_success_when_valid() { - // Arrange var schema = new List { new ListenarrIndexer @@ -116,16 +109,13 @@ public void Test_should_call_testconnection_and_return_success_when_valid() 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(); - // Ensure the private schema cache will execute the factory so it invokes our mocked proxy - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(NzbDrone.Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); + 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); - // Act var result = Subject.Test(); - // Assert result.IsValid.Should().BeTrue(); Mocker.GetMock().Verify(m => m.TestConnection(It.IsAny(), It.IsAny()), Times.Once); } @@ -133,7 +123,6 @@ public void Test_should_call_testconnection_and_return_success_when_valid() [Test] public void Test_should_retry_and_use_fresh_schema_when_cached_schema_is_incomplete() { - // Arrange var cachedSchema = new List { new ListenarrIndexer @@ -161,21 +150,17 @@ public void Test_should_retry_and_use_fresh_schema_when_cached_schema_is_incompl } }; - // Cache returns stale schema - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => cachedSchema); + var cachedForTest = new Mock>>(); + cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) + .Returns>, TimeSpan>((k, f, t) => cachedSchema); - typeof(NzbDrone.Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); + typeof(Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); - // Proxy will return fresh schema when direct fetched 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); - // Act var result = Subject.Test(); - // Assert result.IsValid.Should().BeTrue(); Mocker.GetMock().Verify(m => m.GetIndexerSchema(It.IsAny()), Times.AtLeastOnce); } @@ -183,7 +168,6 @@ public void Test_should_retry_and_use_fresh_schema_when_cached_schema_is_incompl [Test] public void Test_should_handle_exception_from_testconnection() { - // Arrange var schema = new List { new ListenarrIndexer @@ -202,16 +186,13 @@ public void Test_should_handle_exception_from_testconnection() Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); Mocker.GetMock().Setup(c => c.TestConnection(It.IsAny(), It.IsAny())).Throws(new Exception("boom")); - // Ensure the private schema cache will execute the factory so it invokes our mocked proxy - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(NzbDrone.Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); + 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); - // Act var result = Subject.Test(); - // Assert result.IsValid.Should().BeFalse(); result.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("Unable to complete application test")); ExceptionVerification.ExpectedWarns(1); @@ -220,11 +201,9 @@ public void Test_should_handle_exception_from_testconnection() [Test] public void Test_should_fail_when_schema_missing() { - // Arrange Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(new List()); - // Act & Assert - call private BuildListenarrIndexer to assert it throws - var method = typeof(NzbDrone.Core.Applications.Listenarr.Listenarr).GetMethod("BuildListenarrIndexer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + 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 })); @@ -236,7 +215,6 @@ public void Test_should_fail_when_schema_missing() [Test] public void Test_should_fail_when_schema_missing_required_fields() { - // Arrange var schema = new List { new ListenarrIndexer @@ -251,13 +229,11 @@ public void Test_should_fail_when_schema_missing_required_fields() Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); - // Ensure the private schema cache will execute the factory so it invokes our mocked proxy - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(NzbDrone.Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); + 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); - // Act & Assert try { var result = Subject.Test(); @@ -266,7 +242,6 @@ public void Test_should_fail_when_schema_missing_required_fields() } finally { - // Consume expected warnings even if Subject.Test throws or an assert fails so teardown does not fail ExceptionVerification.IgnoreWarns(); } } @@ -274,7 +249,6 @@ public void Test_should_fail_when_schema_missing_required_fields() [Test] public void AddIndexer_should_insert_app_indexer_mapping_on_success() { - // Arrange var indexerDefinition = new IndexerDefinition { Id = 12, @@ -285,7 +259,6 @@ public void AddIndexer_should_insert_app_indexer_mapping_on_success() AppProfile = new LazyLoaded(new AppSyncProfile { EnableRss = true, EnableAutomaticSearch = true, EnableInteractiveSearch = true }) }; - // Add a category that matches Settings.SyncCategories indexerDefinition.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.AudioAudiobook); var mockIndexer = new Mock(); @@ -311,19 +284,15 @@ public void AddIndexer_should_insert_app_indexer_mapping_on_success() 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 }); - // Ensure the private schema cache will execute the factory so it invokes our mocked proxy - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(NzbDrone.Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); + 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); - // pre-check indexerDefinition.Capabilities.Categories.SupportedCategories(((ListenarrSettings)Subject.Definition.Settings).SyncCategories.ToArray()).Should().NotBeEmpty(); - // Act Subject.AddIndexer(indexerDefinition); - // Assert 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()); } @@ -331,7 +300,6 @@ public void AddIndexer_should_insert_app_indexer_mapping_on_success() [Test] public void AddIndexer_should_use_existing_remote_indexer_if_baseUrl_matches() { - // Arrange var indexerDefinition = new IndexerDefinition { Id = 12, @@ -364,7 +332,6 @@ public void AddIndexer_should_use_existing_remote_indexer_if_baseUrl_matches() } }; - // Existing remote indexer with matching baseUrl var existing = new ListenarrIndexer { Id = 501, @@ -381,19 +348,15 @@ public void AddIndexer_should_use_existing_remote_indexer_if_baseUrl_matches() Mocker.GetMock().Setup(c => c.GetIndexerSchema(It.IsAny())).Returns(schema); Mocker.GetMock().Setup(c => c.GetIndexers(It.IsAny())).Returns(new List { existing }); - // Ensure the private schema cache will execute the factory so it invokes our mocked proxy - var cachedForTest = new Mock>>(); - cachedForTest.Setup(c => c.Get(It.IsAny(), It.IsAny>>(), It.IsAny())) - .Returns>, TimeSpan>((k, f, t) => f()); - typeof(NzbDrone.Core.Applications.Listenarr.Listenarr).GetField("_schemaCache", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).SetValue(Subject, cachedForTest.Object); + 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); - // pre-check indexerDefinition.Capabilities.Categories.SupportedCategories(((ListenarrSettings)Subject.Definition.Settings).SyncCategories.ToArray()).Should().NotBeEmpty(); - // Act Subject.AddIndexer(indexerDefinition); - // Assert - AddIndexer should not be called because remote already exists 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 index fbc9007e8..e10bc7c73 100644 --- a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs +++ b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs @@ -4,6 +4,7 @@ using Moq; using NUnit.Framework; using NzbDrone.Common.Http; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Applications.Listenarr; using NzbDrone.Test.Common; @@ -15,10 +16,22 @@ public class ListenarrV1ProxyFixture : TestBase [Test] public void GetIndexers_should_deserialize_json_and_set_api_key_header() { - // Arrange var settings = new ListenarrSettings { BaseUrl = "http://localhost:4545", ApiKey = "abc123" }; - var json = "[ { \"id\": 42, \"name\": \"Test\", \"implementation\": \"Newznab\", \"fields\": [ { \"name\": \"baseUrl\", \"value\": \"http://localhost:4545/1/api\" }, { \"name\": \"apiKey\", \"value\": \"x\" } ] } ]"; + 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; @@ -27,38 +40,31 @@ public void GetIndexers_should_deserialize_json_and_set_api_key_header() .Returns(req => { capturedRequest = req; - return new HttpResponse(req, new HttpHeader { { "Content-Type", "application/json" } }, new CookieCollection(), json, 0, HttpStatusCode.OK, new Version("1.0")); + 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")); }); - // Act var result = Subject.GetIndexers(settings); - // Assert result.Should().NotBeNull(); result.Count.Should().Be(1); capturedRequest.Headers.GetSingleValue("X-Api-Key").Should().Be("abc123"); - // Accept either singular /api/v1/indexer or legacy /api/indexer in requests (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() { - // Arrange var settings = new ListenarrSettings { BaseUrl = "http://localhost:4545", ApiKey = "abc123" }; - // Schema returned as an object with fields as an object (name -> definition) - var json = "{ \"id\": 1, \"implementation\": \"Newznab\", \"fields\": { \"baseUrl\": { \"type\": \"text\" }, \"apiKey\": { \"type\": \"text\" } } }"; + var json = "[ { \"id\": 1, \"implementation\": \"Newznab\", \"fields\": { \"baseUrl\": { \"type\": \"text\" }, \"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"))); - // Act var result = Subject.GetIndexerSchema(settings); - // Assert result.Should().NotBeNull(); result.Count.Should().Be(1); result[0].Fields.Should().NotBeNull(); @@ -68,41 +74,36 @@ public void GetIndexerSchema_should_handle_single_object_response_with_fields_ob } [Test] - public void GetIndexerSchema_should_expand_implementations_array_into_multiple_schemas() + public void GetIndexerSchema_should_preserve_implementations_array_as_list() { - // Arrange var settings = new ListenarrSettings { BaseUrl = "http://localhost:4545", ApiKey = "abc123" }; - var json = "{ \"fields\": [ { \"name\": \"baseUrl\", \"type\": \"text\" } ], \"implementations\": [\"Newznab\",\"Torznab\"] }"; + 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"))); - // Act var result = Subject.GetIndexerSchema(settings); - // Assert result.Should().NotBeNull(); - result.Count.Should().Be(2); - result.Should().Contain(r => r.Implementation == "Newznab"); - result.Should().Contain(r => r.Implementation == "Torznab"); + 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() { - // Arrange 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")))); - // Act / Assert Assert.Throws(() => Subject.GetIndexers(settings)); - // No warning is logged by GetIndexers on unauthorized (it throws before the API-key-specific log path) ExceptionVerification.ExpectedWarns(0); } } diff --git a/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs b/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs index c591df5f3..cdbea5740 100644 --- a/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs +++ b/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs @@ -416,8 +416,8 @@ private ListenarrIndexer BuildListenarrIndexer(IndexerDefinition indexer, Indexe syncFields.AddRange(new List { "additionalParameters" }); } - var newznab = schemas.FirstOrDefault(i => i.Implementation == "Newznab"); - var torznab = schemas.FirstOrDefault(i => i.Implementation == "Torznab"); + 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) { @@ -464,8 +464,8 @@ private ListenarrIndexer BuildListenarrIndexer(IndexerDefinition indexer, Indexe if (freshSchemas != null && freshSchemas.Any()) { - var freshNewznab = freshSchemas.FirstOrDefault(i => i.Implementation == "Newznab"); - var freshTorznab = freshSchemas.FirstOrDefault(i => i.Implementation == "Torznab"); + 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; diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs index 6e18d50c3..ae8634703 100644 --- a/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs +++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs @@ -17,6 +17,7 @@ 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; } diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs index ca7f9da76..104b19d98 100644 --- a/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs +++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs @@ -13,7 +13,6 @@ namespace NzbDrone.Core.Applications.Listenarr { public interface IListenarrV1Proxy { - ListenarrStatus GetStatus(ListenarrSettings settings); ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings); List GetIndexers(ListenarrSettings settings); ListenarrIndexer GetIndexer(int indexerId, ListenarrSettings settings); @@ -39,34 +38,19 @@ public ListenarrV1Proxy(IHttpClient httpClient, Logger logger) _logger = logger; } - public ListenarrStatus GetStatus(ListenarrSettings settings) - { - var request = BuildRequest(settings, $"{AppApiRoute}/system/status", HttpMethod.Get); - return Execute(request); - } - public List GetIndexers(ListenarrSettings settings) { var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get); - try - { - return Execute>(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound) - { - // Fallback to plural resource if the app exposes /indexers - var fallback = BuildRequest(settings, $"{AppApiRoute}/indexers", HttpMethod.Get); - return Execute>(fallback); - } + return Execute>(request); } public ListenarrIndexer GetIndexer(int indexerId, ListenarrSettings settings) { try { - var fallback = BuildRequest(settings, $"{AppApiRoute}/indexers/{indexerId}", HttpMethod.Get); - return Execute(fallback); + var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get); + return Execute(request); } catch (HttpException) { @@ -77,41 +61,26 @@ public ListenarrIndexer GetIndexer(int indexerId, ListenarrSettings settings) public void RemoveIndexer(int indexerId, ListenarrSettings settings) { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete); - - try - { - _httpClient.Execute(request); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound) - { - // Try plural endpoint as fallback - var fallback = BuildRequest(settings, $"{AppApiRoute}/indexers/{indexerId}", HttpMethod.Delete); - _httpClient.Execute(fallback); - } + _httpClient.Execute(request); } public List GetIndexerSchema(ListenarrSettings settings) { var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get); - try + var response = _httpClient.Execute(request); + + if ((int)response.StatusCode >= 300) { - var response = _httpClient.Execute(request); + throw new HttpException(response); + } - if ((int)response.StatusCode >= 300) - { - throw new HttpException(response); - } + var token = Newtonsoft.Json.Linq.JToken.Parse(response.Content); - // Parse and normalize flexible schema responses - var token = Newtonsoft.Json.Linq.JToken.Parse(response.Content); - - // If the schema is a single object, wrap into a list (and expand implementations arrays) - if (token.Type == Newtonsoft.Json.Linq.JTokenType.Object) - { + if (token.Type == Newtonsoft.Json.Linq.JTokenType.Object) + { var obj = (Newtonsoft.Json.Linq.JObject)token; - // Normalize fields when they are returned as an object instead of an array if (obj["fields"] is Newtonsoft.Json.Linq.JObject fieldsObj) { var fieldsArray = new Newtonsoft.Json.Linq.JArray(); @@ -135,27 +104,15 @@ public List GetIndexerSchema(ListenarrSettings settings) obj["fields"] = fieldsArray; } - // If implementations is an array of strings, expand into separate schema entries if (obj["implementations"] is Newtonsoft.Json.Linq.JArray implsArray && implsArray.Count > 0) { - var results = new List(); - - foreach (var impl in implsArray) - { - var copy = (Newtonsoft.Json.Linq.JObject)obj.DeepClone(); - copy.Property("implementations")?.Remove(); - copy["implementation"] = impl; - results.Add(copy.ToObject()); - } - - return results; + return new List { obj.ToObject() }; } return new List { obj.ToObject() }; } - // If it's already an array, parse each item, normalize fields and expand implementations arrays - if (token.Type == Newtonsoft.Json.Linq.JTokenType.Array) + if (token.Type == Newtonsoft.Json.Linq.JTokenType.Array) { var list = new List(); @@ -168,7 +125,6 @@ public List GetIndexerSchema(ListenarrSettings settings) var obj = (Newtonsoft.Json.Linq.JObject)item; - // Normalize fields if needed if (obj["fields"] is Newtonsoft.Json.Linq.JObject fieldsObj2) { var fieldsArray = new Newtonsoft.Json.Linq.JArray(); @@ -191,96 +147,17 @@ public List GetIndexerSchema(ListenarrSettings settings) obj["fields"] = fieldsArray; } - if (obj["implementations"] is Newtonsoft.Json.Linq.JArray impls) - { - foreach (var impl in impls) - { - var copy = (Newtonsoft.Json.Linq.JObject)obj.DeepClone(); - copy.Property("implementations")?.Remove(); - copy["implementation"] = impl; - list.Add(copy.ToObject()); - } - } - else - { - list.Add(obj.ToObject()); - } + list.Add(obj.ToObject()); } return list; } - // Unexpected token type - throw new JsonReaderException("Unexpected JSON token while parsing Listenarr schema"); - } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound) - { - var fallback = BuildRequest(settings, $"{AppApiRoute}/indexers/schema", HttpMethod.Get); - var fallbackResponse = _httpClient.Execute(fallback); - - if ((int)fallbackResponse.StatusCode >= 300) - { - throw new HttpException(fallbackResponse); - } - - var token = Newtonsoft.Json.Linq.JToken.Parse(fallbackResponse.Content); - - if (token.Type == Newtonsoft.Json.Linq.JTokenType.Array) - { - return token.ToObject>(); - } - - if (token.Type == Newtonsoft.Json.Linq.JTokenType.Object) - { - var obj = (Newtonsoft.Json.Linq.JObject)token; - - if (obj["fields"] is Newtonsoft.Json.Linq.JObject fieldsObj) - { - var fieldsArray = new Newtonsoft.Json.Linq.JArray(); - - foreach (var prop in fieldsObj.Properties()) - { - if (prop.Value.Type == Newtonsoft.Json.Linq.JTokenType.Object) - { - var item = (Newtonsoft.Json.Linq.JObject)prop.Value; - item["name"] = prop.Name; - fieldsArray.Add(item); - } - else - { - var item = new Newtonsoft.Json.Linq.JObject { ["name"] = prop.Name, ["value"] = prop.Value }; - fieldsArray.Add(item); - } - } - - obj["fields"] = fieldsArray; - } - - if (obj["implementations"] is Newtonsoft.Json.Linq.JArray implsArray && implsArray.Count > 0) - { - var results = new List(); - - foreach (var impl in implsArray) - { - var copy = (Newtonsoft.Json.Linq.JObject)obj.DeepClone(); - copy.Property("implementations")?.Remove(); - copy["implementation"] = impl; - results.Add(copy.ToObject()); - } - - return results; - } - - return new List { obj.ToObject() }; - } - - throw new JsonReaderException("Unexpected JSON token while parsing Listenarr schema (fallback)"); - } + throw new JsonReaderException("Unexpected JSON token while parsing Listenarr schema"); } public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings) { - // Defensive check: avoid creating duplicates if an indexer with the same baseUrl already exists on the remote app. try { var incomingBaseUrl = indexer?.Fields?.FirstOrDefault(f => f.Name == "baseUrl")?.Value as string; @@ -305,7 +182,6 @@ public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings s } catch (Exception ex) { - // If the existence check fails for any reason, proceed with the add flow and let any resulting errors bubble up. _logger.Debug(ex, "Failed to run pre-flight existence check before AddIndexer; proceeding to create"); } @@ -326,22 +202,6 @@ public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings s _logger.Debug("Retry payload: {0}", request.ContentSummary); return ExecuteIndexerRequest(request); } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound) - { - // Try plural form as a fallback - var fallback = BuildRequest(settings, $"{AppApiRoute}/indexers", HttpMethod.Post); - fallback.SetContent(indexer.ToJson()); - fallback.ContentSummary = indexer.ToJson(Formatting.None); - - try - { - return ExecuteIndexerRequest(fallback); - } - catch (HttpException) - { - throw; - } - } } public ListenarrIndexer UpdateIndexer(ListenarrIndexer indexer, ListenarrSettings settings) @@ -361,22 +221,6 @@ public ListenarrIndexer UpdateIndexer(ListenarrIndexer indexer, ListenarrSetting request.Url = request.Url.AddQueryParam("forceSave", "true"); return ExecuteIndexerRequest(request); } - catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.NotFound) - { - // Try plural form as a fallback - var fallback = BuildRequest(settings, $"{AppApiRoute}/indexers/{indexer.Id}", HttpMethod.Put); - fallback.SetContent(indexer.ToJson()); - fallback.ContentSummary = indexer.ToJson(Formatting.None); - - try - { - return ExecuteIndexerRequest(fallback); - } - catch (HttpException) - { - throw; - } - } } public ValidationFailure TestConnection(ListenarrIndexer indexer, ListenarrSettings settings) @@ -390,16 +234,6 @@ public ValidationFailure TestConnection(ListenarrIndexer indexer, ListenarrSetti { var applicationVersion = _httpClient.Post(request).Headers.GetSingleValue("X-Application-Version"); - if (applicationVersion == null) - { - // Try plural endpoint as a fallback - var fallback = BuildRequest(settings, $"{AppApiRoute}/indexers/test", HttpMethod.Post); - fallback.SetContent(indexer.ToJson()); - fallback.ContentSummary = indexer.ToJson(Formatting.None); - - applicationVersion = _httpClient.Post(fallback).Headers.GetSingleValue("X-Application-Version"); - } - if (applicationVersion == null) { return new ValidationFailure(string.Empty, "Failed to fetch Listenarr version");