diff --git a/frontend/src/Settings/Applications/Listenarr/Listenarr.tsx b/frontend/src/Settings/Applications/Listenarr/Listenarr.tsx
new file mode 100644
index 000000000..21f2294b7
--- /dev/null
+++ b/frontend/src/Settings/Applications/Listenarr/Listenarr.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Application } from 'Settings/Applications/Application';
+import ProviderSettingsModal from 'Settings/Applications/ProviderSettingsModal';
+
+interface ListenarrProps {
+ selectedApplication: Application;
+ onModalClose: () => void;
+}
+
+function Listenarr({ selectedApplication, onModalClose }: ListenarrProps) {
+ return (
+
+ );
+}
+
+export default Listenarr;
diff --git a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs
new file mode 100644
index 000000000..8cb541f93
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrFixture.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+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:5000",
+ ApiKey = "abc",
+ SyncCategories = new List { NewznabStandardCategory.Movies.Id }
+ }
+ };
+
+ Mocker.GetMock().SetupGet(c => c.ApiKey).Returns("abc");
+ }
+
+ [Test]
+ public void GetIndexerMappings_should_return_mappings_when_baseUrl_matches_prowlarr()
+ {
+ // Arrange
+ 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 });
+
+ // Act
+ var mappings = Subject.GetIndexerMappings();
+
+ // Assert
+ 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()
+ {
+ // Arrange
+ 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 });
+
+ // Act
+ var mappings = Subject.GetIndexerMappings();
+
+ // Assert
+ mappings.Should().BeEmpty();
+ }
+
+ [Test]
+ public void Test_should_fail_when_status_null()
+ {
+ // Arrange
+ Mocker.GetMock().Setup(c => c.GetStatus(It.IsAny())).Returns((ListenarrStatus)null);
+
+ // Act
+ var result = Subject.Test();
+
+ // Assert
+ result.IsValid.Should().BeFalse();
+ result.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("Unable to connect to Listenarr"));
+ }
+
+ [Test]
+ public void Test_should_fail_on_exception()
+ {
+ // Arrange
+ Mocker.GetMock().Setup(c => c.GetStatus(It.IsAny())).Throws(new Exception("boom"));
+
+ // Act
+ var result = Subject.Test();
+
+ // Assert
+ result.IsValid.Should().BeFalse();
+ result.Errors.Should().ContainSingle(e => e.ErrorMessage.Contains("Unable to send test message"));
+
+ // expected error was logged
+ ExceptionVerification.ExpectedErrors(1);
+ }
+
+ [Test]
+ public void AddIndexer_should_insert_app_indexer_mapping_on_success()
+ {
+ // Arrange
+ 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 })
+ };
+
+ // Add a category that matches Settings.SyncCategories
+ indexerDefinition.Capabilities.Categories.AddCategoryMapping(1, NewznabStandardCategory.Movies);
+
+ var mockIndexer = new Mock();
+ mockIndexer.Setup(i => i.GetCapabilities()).Returns(indexerDefinition.Capabilities);
+
+ Mocker.GetMock().Setup(m => m.GetInstance(It.IsAny())).Returns(mockIndexer.Object);
+
+ Mocker.GetMock().Setup(c => c.AddIndexer(It.IsAny(), It.IsAny())).Returns(new ListenarrIndexer { Id = 501 });
+
+ // 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());
+ }
+ }
+}
diff --git a/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs
new file mode 100644
index 000000000..10ce96084
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Applications/Listenarr/ListenarrV1ProxyFixture.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Net;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Common.Http;
+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()
+ {
+ // Arrange
+ var settings = new ListenarrSettings { BaseUrl = "http://localhost:5000", ApiKey = "abc123" };
+
+ var json = "[ { \"id\": 42, \"name\": \"Test\", \"implementation\": \"Newznab\", \"fields\": [ { \"name\": \"baseUrl\", \"value\": \"http://localhost:5000/1/api\" }, { \"name\": \"apiKey\", \"value\": \"x\" } ] } ]";
+
+ 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(), json, 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");
+ capturedRequest.Url.ToString().Should().Contain("/api/indexer");
+ }
+
+ [Test]
+ public void Execute_should_throw_application_exception_when_unauthorized()
+ {
+ // Arrange
+ var settings = new ListenarrSettings { BaseUrl = "http://localhost:5000", 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));
+
+ // expected error was logged
+ ExceptionVerification.ExpectedErrors(1);
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs b/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs
new file mode 100644
index 000000000..06f71b1f6
--- /dev/null
+++ b/src/NzbDrone.Core/Applications/Listenarr/Listenarr.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FluentValidation.Results;
+using NLog;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Core.Configuration;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.Tags;
+
+namespace NzbDrone.Core.Applications.Listenarr
+{
+ public class Listenarr : ApplicationBase
+ {
+ public override string Name => "Listenarr";
+
+ private readonly IListenarrV1Proxy _listenarrV1Proxy;
+ private readonly IConfigFileProvider _configFileProvider;
+ private readonly Lazy _tagService;
+
+ public Listenarr(
+ IListenarrV1Proxy listenarrV1Proxy,
+ IAppIndexerMapService appIndexerMapService,
+ IIndexerFactory indexerFactory,
+ IConfigFileProvider configFileProvider,
+ Lazy tagService,
+ Logger logger)
+ : base(appIndexerMapService, indexerFactory, logger)
+ {
+ _listenarrV1Proxy = listenarrV1Proxy;
+ _configFileProvider = configFileProvider;
+ _tagService = tagService;
+ }
+
+ public override ValidationResult Test()
+ {
+ var failures = new List();
+
+ try
+ {
+ var status = _listenarrV1Proxy.GetStatus(Settings);
+
+ if (status == null)
+ {
+ failures.Add(new ValidationFailure(string.Empty, "Unable to connect to Listenarr"));
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Unable to send test message");
+ failures.Add(new ValidationFailure(string.Empty, "Unable to send test message"));
+ }
+
+ return new ValidationResult(failures);
+ }
+
+ public override List GetIndexerMappings()
+ {
+ var indexers = _listenarrV1Proxy.GetIndexers(Settings)?.Where(i => i.Implementation is "Newznab" or "Torznab");
+ var mappings = new List();
+
+ foreach (var indexer in indexers ?? Enumerable.Empty())
+ {
+ var baseUrl = indexer.Fields?.FirstOrDefault(x => x.Name == "baseUrl")?.Value?.ToString() ?? indexer.BaseUrl ?? string.Empty;
+
+ if (!baseUrl.StartsWith(Settings.ProwlarrUrl.TrimEnd('/')) &&
+ (string)indexer.Fields?.FirstOrDefault(x => x.Name == "apiKey")?.Value != _configFileProvider.ApiKey)
+ {
+ continue;
+ }
+
+ var match = AppIndexerRegex.Match(baseUrl);
+
+ if (match.Groups["indexer"].Success && int.TryParse(match.Groups["indexer"].Value, out var indexerId))
+ {
+ mappings.Add(new AppIndexerMap
+ {
+ IndexerId = indexerId,
+ RemoteIndexerId = indexer.Id
+ });
+ }
+ }
+
+ return mappings;
+ }
+
+ public override void AddIndexer(IndexerDefinition indexer)
+ {
+ var indexerCapabilities = GetIndexerCapabilities(indexer);
+
+ if (!indexerCapabilities.SearchAvailable)
+ {
+ _logger.Debug("Skipping add for indexer {0} [{1}] due to missing search support by the indexer", indexer.Name, indexer.Id);
+ return;
+ }
+
+ if (indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Empty())
+ {
+ _logger.Debug("Skipping add for indexer {0} [{1}] due to no app Sync Categories supported by the indexer", indexer.Name, indexer.Id);
+ return;
+ }
+
+ _logger.Trace("Adding indexer {0} [{1}]", indexer.Name, indexer.Id);
+
+ var listenarrIndexer = BuildListenarrIndexer(indexer, Settings);
+
+ var remoteIndexer = _listenarrV1Proxy.AddIndexer(listenarrIndexer, Settings);
+
+ if (remoteIndexer == null)
+ {
+ _logger.Debug("Failed to add {0} [{1}]", indexer.Name, indexer.Id);
+ return;
+ }
+
+ _appIndexerMapService.Insert(new AppIndexerMap
+ {
+ AppId = Definition.Id,
+ IndexerId = indexer.Id,
+ RemoteIndexerId = remoteIndexer.Id
+ });
+ }
+
+ public override void RemoveIndexer(int indexerId)
+ {
+ var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
+ var mapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
+
+ if (mapping != null)
+ {
+ _listenarrV1Proxy.RemoveIndexer(mapping.RemoteIndexerId, Settings);
+ _appIndexerMapService.Delete(mapping.Id);
+ }
+ }
+
+ public override void UpdateIndexer(IndexerDefinition indexer, bool forceSync = false)
+ {
+ _logger.Debug("Updating indexer {0} [{1}]", indexer.Name, indexer.Id);
+
+ var indexerCapabilities = GetIndexerCapabilities(indexer);
+ var appMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
+ var mapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
+
+ if (mapping != null)
+ {
+ var listenarrIndexer = BuildListenarrIndexer(indexer, Settings, mapping.RemoteIndexerId);
+ _listenarrV1Proxy.UpdateIndexer(listenarrIndexer, Settings);
+ }
+ }
+
+ public override object RequestAction(string action, IDictionary query)
+ {
+ if (action == "getTags")
+ {
+ var tags = _tagService.Value.All().Select(t => new { Value = t.Id, Name = t.Label });
+ return new { options = tags };
+ }
+
+ return base.RequestAction(action, query);
+ }
+
+ private ListenarrIndexer BuildListenarrIndexer(IndexerDefinition indexer, ListenarrSettings settings, int remoteId = 0)
+ {
+ var listenarrIndexer = new ListenarrIndexer
+ {
+ Id = remoteId,
+ Name = $"{indexer.Name} (Prowlarr)",
+ EnableRss = indexer.Enable && indexer.AppProfile.Value.EnableRss,
+ EnableAutomaticSearch = indexer.Enable && indexer.AppProfile.Value.EnableAutomaticSearch,
+ EnableInteractiveSearch = indexer.Enable && indexer.AppProfile.Value.EnableInteractiveSearch,
+ Priority = indexer.Priority,
+ ConfigContract = "NewznabSettings",
+ Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
+ Protocol = indexer.Protocol == DownloadProtocol.Usenet ? "usenet" : "torrent",
+ Fields = new List
+ {
+ new ListenarrField
+ {
+ Name = "baseUrl",
+ Value = $"{settings.ProwlarrUrl.TrimEnd('/')}/{indexer.Id}/api"
+ },
+ new ListenarrField
+ {
+ Name = "apiKey",
+ Value = _configFileProvider.ApiKey
+ },
+ new ListenarrField
+ {
+ Name = "categories",
+ Value = Array.Empty()
+ }
+ }
+ };
+
+ return listenarrIndexer;
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs
new file mode 100644
index 000000000..ae70fa9c3
--- /dev/null
+++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrModels.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+
+namespace NzbDrone.Core.Applications.Listenarr
+{
+ public class ListenarrStatus
+ {
+ public string Version { get; set; }
+ }
+
+ public class ListenarrIndexer
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public bool EnableRss { get; set; }
+ public bool EnableAutomaticSearch { get; set; }
+ public bool EnableInteractiveSearch { get; set; }
+ public int Priority { get; set; }
+ public string ConfigContract { get; set; }
+ public string Implementation { get; set; }
+ public string Protocol { get; set; }
+ public string BaseUrl { get; set; }
+ public List Fields { get; set; }
+ public List Tags { get; set; }
+ }
+
+ public class ListenarrField
+ {
+ public string Name { get; set; }
+ public object Value { get; set; }
+ }
+
+ public class ListenarrTag
+ {
+ public int Id { get; set; }
+ public string Label { get; set; }
+ }
+}
diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrSettings.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrSettings.cs
new file mode 100644
index 000000000..201a73c50
--- /dev/null
+++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrSettings.cs
@@ -0,0 +1,52 @@
+using System.Collections.Generic;
+using FluentValidation;
+using NzbDrone.Core.Annotations;
+using NzbDrone.Core.Validation;
+
+namespace NzbDrone.Core.Applications.Listenarr
+{
+ public class ListenarrSettingsValidator : AbstractValidator
+ {
+ public ListenarrSettingsValidator()
+ {
+ RuleFor(c => c.BaseUrl).IsValidUrl();
+ RuleFor(c => c.ProwlarrUrl).IsValidUrl();
+ RuleFor(c => c.ApiKey).NotEmpty();
+ }
+ }
+
+ public class ListenarrSettings : IApplicationSettings
+ {
+ private static readonly ListenarrSettingsValidator Validator = new ListenarrSettingsValidator();
+
+ public ListenarrSettings()
+ {
+ ProwlarrUrl = "http://localhost:9696";
+ BaseUrl = "http://localhost:5000";
+ SyncLevel = (int)ApplicationSyncLevel.FullSync; // default reasonable behavior
+ }
+
+ [FieldDefinition(0, Label = "Prowlarr Server", HelpText = "URL of Prowlarr server as Listenarr sees it, including http:// or https://, and port if needed")]
+ public string ProwlarrUrl { get; set; }
+
+ [FieldDefinition(1, Label = "Listenarr Server", HelpText = "URL of Listenarr server, including http:// or https://, and port if needed")]
+ public string BaseUrl { get; set; }
+
+ [FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "API Key for Listenarr")]
+ public string ApiKey { get; set; }
+
+ [FieldDefinition(3, Type = FieldType.Select, SelectOptions = typeof(ApplicationSyncLevel), Label = "Sync Level", HelpText = "How should Prowlarr sync indexers to Listenarr")]
+ public int SyncLevel { get; set; }
+
+ [FieldDefinition(4, Label = "Sync Categories", Advanced = true, HelpText = "Sync audiobook categories to Listenarr (must match Listenarr's category schema)")]
+ public IEnumerable SyncCategories { get; set; }
+
+ [FieldDefinition(5, Type = FieldType.TagSelect, SelectOptionsProviderAction = "getTags", Label = "Tags", HelpText = "Only add indexers with these tags to Listenarr")]
+ public IEnumerable Tags { get; set; }
+
+ public NzbDroneValidationResult Validate()
+ {
+ return new NzbDroneValidationResult(Validator.Validate(this));
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs b/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs
new file mode 100644
index 000000000..101fa1bac
--- /dev/null
+++ b/src/NzbDrone.Core/Applications/Listenarr/ListenarrV1Proxy.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
+using Newtonsoft.Json;
+using NLog;
+using NzbDrone.Common.Http;
+using NzbDrone.Common.Serializer;
+
+namespace NzbDrone.Core.Applications.Listenarr
+{
+ public interface IListenarrV1Proxy
+ {
+ ListenarrStatus GetStatus(ListenarrSettings settings);
+ List GetIndexers(ListenarrSettings settings);
+ ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings);
+ void UpdateIndexer(ListenarrIndexer indexer, ListenarrSettings settings);
+ void RemoveIndexer(int indexerId, ListenarrSettings settings);
+ List GetTags(ListenarrSettings settings);
+ }
+
+ public class ListenarrV1Proxy : IListenarrV1Proxy
+ {
+ private readonly IHttpClient _httpClient;
+ private readonly Logger _logger;
+
+ public ListenarrV1Proxy(IHttpClient httpClient, Logger logger)
+ {
+ _httpClient = httpClient;
+ _logger = logger;
+ }
+
+ public ListenarrStatus GetStatus(ListenarrSettings settings)
+ {
+ var request = BuildRequest(settings, "/api/system/status", HttpMethod.Get);
+ return Execute(request);
+ }
+
+ public List GetIndexers(ListenarrSettings settings)
+ {
+ var request = BuildRequest(settings, "/api/indexer", HttpMethod.Get);
+ return Execute>(request);
+ }
+
+ public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings)
+ {
+ var request = BuildRequest(settings, "/api/indexer", HttpMethod.Post);
+ request.SetContent(indexer.ToJson());
+ request.ContentSummary = indexer.ToJson(Formatting.None);
+ return Execute(request);
+ }
+
+ public void UpdateIndexer(ListenarrIndexer indexer, ListenarrSettings settings)
+ {
+ var request = BuildRequest(settings, $"/api/indexer/{indexer.Id}", HttpMethod.Put);
+ request.SetContent(indexer.ToJson());
+ request.ContentSummary = indexer.ToJson(Formatting.None);
+ Execute(request);
+ }
+
+ public void RemoveIndexer(int indexerId, ListenarrSettings settings)
+ {
+ var request = BuildRequest(settings, $"/api/indexer/{indexerId}", HttpMethod.Delete);
+ _httpClient.Execute(request);
+ }
+
+ public List GetTags(ListenarrSettings settings)
+ {
+ var request = BuildRequest(settings, "/api/tag", HttpMethod.Get);
+ return Execute>(request);
+ }
+
+ private HttpRequest BuildRequest(ListenarrSettings settings, string resource, HttpMethod method)
+ {
+ var baseUrl = settings.BaseUrl.TrimEnd('/');
+
+ var request = new HttpRequestBuilder(baseUrl)
+ .Resource(resource)
+ .Accept(HttpAccept.Json)
+ .SetHeader("X-Api-Key", settings.ApiKey)
+ .Build();
+
+ request.Headers.ContentType = "application/json";
+ request.Method = method;
+ request.AllowAutoRedirect = true;
+
+ return request;
+ }
+
+ private T Execute(HttpRequest request)
+ where T : new()
+ {
+ try
+ {
+ var response = _httpClient.Execute(request);
+
+ if ((int)response.StatusCode >= 300)
+ {
+ throw new HttpException(response);
+ }
+
+ return Json.Deserialize(response.Content);
+ }
+ catch (HttpException ex)
+ {
+ if (ex.Response != null && ex.Response.StatusCode == HttpStatusCode.Unauthorized)
+ {
+ _logger.Error(ex, "API Key is invalid");
+ throw new ApplicationException("API Key is invalid");
+ }
+
+ throw;
+ }
+ }
+ }
+}