This commit is contained in:
Robbie Davis 2026-05-02 08:17:44 -04:00 committed by GitHub
commit dc9195612b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 625 additions and 0 deletions

View file

@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using FluentValidation.Results;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
namespace NzbDrone.Core.Applications.Listenarr
{
public class Listenarr : ApplicationBase<ListenarrSettings>
{
public override string Name => "Listenarr";
private readonly IListenarrV1Proxy _listenarrV1Proxy;
private readonly ICached<List<ListenarrIndexer>> _schemaCache;
private readonly IConfigFileProvider _configFileProvider;
public Listenarr(ICacheManager cacheManager, IListenarrV1Proxy listenarrV1Proxy, IConfigFileProvider configFileProvider, IAppIndexerMapService appIndexerMapService, IIndexerFactory indexerFactory, Logger logger)
: base(appIndexerMapService, indexerFactory, logger)
{
_schemaCache = cacheManager.GetCache<List<ListenarrIndexer>>(GetType());
_listenarrV1Proxy = listenarrV1Proxy;
_configFileProvider = configFileProvider;
}
public override ValidationResult Test()
{
var failures = new List<ValidationFailure>();
var testIndexer = new IndexerDefinition
{
Id = 0,
Name = "Test",
Protocol = DownloadProtocol.Usenet,
Capabilities = new IndexerCapabilities()
};
foreach (var cat in NewznabStandardCategory.AllCats)
{
testIndexer.Capabilities.Categories.AddCategoryMapping(1, cat);
}
try
{
failures.AddIfNotNull(_listenarrV1Proxy.TestConnection(BuildListenarrIndexer(testIndexer, testIndexer.Capabilities, DownloadProtocol.Usenet), Settings));
}
catch (HttpException ex)
{
switch (ex.Response.StatusCode)
{
case HttpStatusCode.Unauthorized:
_logger.Warn(ex, "API Key is invalid");
failures.AddIfNotNull(new ValidationFailure("ApiKey", "API Key is invalid"));
break;
case HttpStatusCode.BadRequest:
_logger.Warn(ex, "Prowlarr URL is invalid");
failures.AddIfNotNull(new ValidationFailure("ProwlarrUrl", "Prowlarr URL is invalid, Listenarr cannot connect to Prowlarr"));
break;
case HttpStatusCode.SeeOther:
case HttpStatusCode.TemporaryRedirect:
_logger.Warn(ex, "Listenarr returned redirect and is invalid");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", "Listenarr URL is invalid, Prowlarr cannot connect to Listenarr - are you missing a URL base?"));
break;
default:
_logger.Warn(ex, "Unable to complete application test");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Listenarr. {ex.Message}"));
break;
}
}
catch (JsonReaderException ex)
{
_logger.Error(ex, "Unable to parse JSON response from application");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to parse JSON response from application. {ex.Message}"));
}
catch (Exception ex)
{
_logger.Warn(ex, "Unable to complete application test");
failures.AddIfNotNull(new ValidationFailure("BaseUrl", $"Unable to complete application test, cannot connect to Listenarr. {ex.Message}"));
}
return new ValidationResult(failures);
}
public override List<AppIndexerMap> GetIndexerMappings()
{
var indexers = _listenarrV1Proxy.GetIndexers(Settings)
.Where(i => i.Implementation is "Newznab" or "Torznab");
var mappings = new List<AppIndexerMap>();
foreach (var indexer in indexers)
{
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)
{
continue;
}
var match = AppIndexerRegex.Match(baseUrl);
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 });
}
}
return mappings;
}
public override void AddIndexer(IndexerDefinition indexer)
{
var indexerCapabilities = GetIndexerCapabilities(indexer);
if (!indexerCapabilities.MusicSearchAvailable && !indexerCapabilities.SearchAvailable)
{
_logger.Debug("Skipping add for indexer {0} [{1}] due to missing music or basic 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, indexerCapabilities, indexer.Protocol);
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 indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexerId);
if (indexerMapping != null)
{
//Remove Indexer remotely and then remove the mapping
_listenarrV1Proxy.RemoveIndexer(indexerMapping.RemoteIndexerId, Settings);
_appIndexerMapService.Delete(indexerMapping.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 indexerMapping = appMappings.FirstOrDefault(m => m.IndexerId == indexer.Id);
var listenarrIndexer = BuildListenarrIndexer(indexer, indexerCapabilities, indexer.Protocol, indexerMapping?.RemoteIndexerId ?? 0);
var remoteIndexer = _listenarrV1Proxy.GetIndexer(indexerMapping.RemoteIndexerId, Settings);
if (remoteIndexer != null)
{
_logger.Debug("Remote indexer {0} [{1}] found", remoteIndexer.Name, remoteIndexer.Id);
if (!listenarrIndexer.Equals(remoteIndexer) || forceSync)
{
_logger.Debug("Syncing remote indexer with current settings");
if ((indexerCapabilities.MusicSearchAvailable || indexerCapabilities.SearchAvailable) &&
indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
// Retain user fields not-affiliated with Prowlarr
listenarrIndexer.Fields.AddRange(remoteIndexer.Fields.Where(f => listenarrIndexer.Fields.All(s => s.Name != f.Name)));
// Retain user tags not-affiliated with Prowlarr
listenarrIndexer.Tags.UnionWith(remoteIndexer.Tags);
// Retain user settings not-affiliated with Prowlarr
listenarrIndexer.DownloadClientId = remoteIndexer.DownloadClientId;
// 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);
}
}
}
else
{
_appIndexerMapService.Delete(indexerMapping.Id);
if ((indexerCapabilities.MusicSearchAvailable || indexerCapabilities.SearchAvailable) &&
indexerCapabilities.Categories.SupportedCategories(Settings.SyncCategories.ToArray()).Any())
{
_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);
_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);
}
}
}
private ListenarrIndexer BuildListenarrIndexer(IndexerDefinition indexer, IndexerCapabilities indexerCapabilities, DownloadProtocol protocol, int id = 0)
{
var cacheKey = $"{Settings.BaseUrl}";
var schemas = _schemaCache.Get(cacheKey, () => _listenarrV1Proxy.GetIndexerSchema(Settings), TimeSpan.FromDays(7));
var syncFields = new List<string> { "baseUrl", "apiPath", "apiKey", "categories", "minimumSeeders", "seedCriteria.seedRatio", "seedCriteria.seedTime" };
var newznab = schemas.First(i => i.Implementation == "Newznab");
var torznab = schemas.First(i => i.Implementation == "Torznab");
var schema = protocol == DownloadProtocol.Usenet ? newznab : torznab;
var listenarrIndexer = new ListenarrIndexer
{
Id = id,
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,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract,
Fields = new List<ListenarrField>(),
Tags = new HashSet<int>()
};
listenarrIndexer.Fields.AddRange(schema.Fields.Where(x => syncFields.Contains(x.Name)));
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;
}
}
}

View file

@ -0,0 +1,17 @@
namespace NzbDrone.Core.Applications.Listenarr
{
public class ListenarrField
{
public string Name { get; set; }
public object Value { get; set; }
public string Type { get; set; }
public bool Advanced { get; set; }
public string Section { get; set; }
public string Hidden { get; set; }
public ListenarrField Clone()
{
return (ListenarrField)MemberwiseClone();
}
}
}

View file

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace NzbDrone.Core.Applications.Listenarr
{
public class ListenarrIndexer
{
public int Id { get; set; }
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public int Priority { get; set; }
public string Name { get; set; }
public string ImplementationName { get; set; }
public string Implementation { get; set; }
public string ConfigContract { get; set; }
public string InfoLink { get; set; }
public int? DownloadClientId { get; set; }
public HashSet<int> Tags { get; set; }
public List<ListenarrField> Fields { get; set; }
public bool Equals(ListenarrIndexer other)
{
if (ReferenceEquals(null, other))
{
return false;
}
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);
var apiKey = (string)Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value;
var otherApiKey = (string)other.Fields.FirstOrDefault(x => x.Name == "apiKey")?.Value;
var apiKeyCompare = apiKey == otherApiKey || otherApiKey == "********";
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);
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;
}
}
}

View file

@ -0,0 +1,47 @@
using System.Collections.Generic;
using FluentValidation;
using NzbDrone.Core.Annotations;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Validation;
namespace NzbDrone.Core.Applications.Listenarr
{
public class ListenarrSettingsValidator : AbstractValidator<ListenarrSettings>
{
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();
public ListenarrSettings()
{
ProwlarrUrl = "http://localhost:9696";
BaseUrl = "http://localhost:4545";
SyncCategories = new[] { 3030 };
}
[FieldDefinition(0, Label = "Prowlarr Server", HelpText = "Prowlarr server URL as Listenarr sees it, including http(s)://, port, and urlbase if needed", Placeholder = "http://localhost:9696")]
public string ProwlarrUrl { get; set; }
[FieldDefinition(1, Label = "Listenarr Server", HelpText = "URL used to connect to Listenarr server, including http(s)://, port, and urlbase if required", Placeholder = "http://localhost:4545")]
public string BaseUrl { get; set; }
[FieldDefinition(2, Label = "API Key", Privacy = PrivacyLevel.ApiKey, HelpText = "The ApiKey generated by Listenarr in Settings/General")]
public string ApiKey { get; set; }
[FieldDefinition(3, Label = "Sync Categories", Type = FieldType.Select, SelectOptions = typeof(NewznabCategoryFieldConverter), HelpText = "Only Indexers that support these categories will be synced", Advanced = true)]
public IEnumerable<int> SyncCategories { get; set; }
public NzbDroneValidationResult Validate()
{
return new NzbDroneValidationResult(Validator.Validate(this));
}
}
}

View file

@ -0,0 +1,7 @@
namespace NzbDrone.Core.Applications.Listenarr
{
public class ListenarrStatus
{
public string Version { get; set; }
}
}

View file

@ -0,0 +1,225 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using FluentValidation.Results;
using Newtonsoft.Json;
using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Common.Serializer;
namespace NzbDrone.Core.Applications.Listenarr
{
public interface IListenarrV1Proxy
{
ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings);
List<ListenarrIndexer> GetIndexers(ListenarrSettings settings);
ListenarrIndexer GetIndexer(int indexerId, ListenarrSettings settings);
List<ListenarrIndexer> GetIndexerSchema(ListenarrSettings settings);
void RemoveIndexer(int indexerId, ListenarrSettings settings);
ListenarrIndexer UpdateIndexer(ListenarrIndexer indexer, ListenarrSettings settings);
ValidationFailure TestConnection(ListenarrIndexer indexer, ListenarrSettings settings);
}
public class ListenarrV1Proxy : IListenarrV1Proxy
{
private static Version MinimumApplicationVersion => new(0, 2, 66, 0);
private const string AppApiRoute = "/api/v1";
private const string AppIndexerApiRoute = $"{AppApiRoute}/prowlarr/indexer";
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, $"{AppApiRoute}/system/status", HttpMethod.Get);
return Execute<ListenarrStatus>(request);
}
public List<ListenarrIndexer> GetIndexers(ListenarrSettings settings)
{
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Get);
return Execute<List<ListenarrIndexer>>(request);
}
public ListenarrIndexer GetIndexer(int indexerId, ListenarrSettings settings)
{
try
{
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Get);
return Execute<ListenarrIndexer>(request);
}
catch (HttpException ex)
{
if (ex.Response.StatusCode != HttpStatusCode.NotFound)
{
throw;
}
}
return null;
}
public void RemoveIndexer(int indexerId, ListenarrSettings settings)
{
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexerId}", HttpMethod.Delete);
_httpClient.Execute(request);
}
public List<ListenarrIndexer> GetIndexerSchema(ListenarrSettings settings)
{
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/schema", HttpMethod.Get);
return Execute<List<ListenarrIndexer>>(request);
}
public ListenarrIndexer AddIndexer(ListenarrIndexer indexer, ListenarrSettings settings)
{
var request = BuildRequest(settings, $"{AppIndexerApiRoute}", HttpMethod.Post);
request.SetContent(indexer.ToJson());
request.ContentSummary = indexer.ToJson(Formatting.None);
try
{
return ExecuteIndexerRequest(request);
}
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Debug("Retrying to add indexer forcefully");
request.Url = request.Url.AddQueryParam("forceSave", "true");
return ExecuteIndexerRequest(request);
}
}
public ListenarrIndexer UpdateIndexer(ListenarrIndexer indexer, ListenarrSettings settings)
{
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/{indexer.Id}", HttpMethod.Put);
request.SetContent(indexer.ToJson());
request.ContentSummary = indexer.ToJson(Formatting.None);
try
{
return ExecuteIndexerRequest(request);
}
catch (HttpException ex) when (ex.Response.StatusCode == HttpStatusCode.BadRequest)
{
_logger.Debug("Retrying to update indexer forcefully");
request.Url = request.Url.AddQueryParam("forceSave", "true");
return ExecuteIndexerRequest(request);
}
}
public ValidationFailure TestConnection(ListenarrIndexer indexer, ListenarrSettings settings)
{
var request = BuildRequest(settings, $"{AppIndexerApiRoute}/test", HttpMethod.Post);
request.SetContent(indexer.ToJson());
request.ContentSummary = indexer.ToJson(Formatting.None);
var applicationVersion = _httpClient.Post(request).Headers.GetSingleValue("X-Application-Version");
if (applicationVersion == null)
{
return new ValidationFailure(string.Empty, "Failed to fetch Listenarr version");
}
if (new Version(applicationVersion) < MinimumApplicationVersion)
{
return new ValidationFailure(string.Empty, $"Listenarr version should be at least {MinimumApplicationVersion.ToString(3)}. Version reported is {applicationVersion}", applicationVersion);
}
return null;
}
private ListenarrIndexer ExecuteIndexerRequest(HttpRequest request)
{
try
{
return Execute<ListenarrIndexer>(request);
}
catch (HttpException ex)
{
switch (ex.Response.StatusCode)
{
case HttpStatusCode.Unauthorized:
_logger.Warn(ex, "API Key is invalid");
break;
case HttpStatusCode.BadRequest:
if (ex.Response.Content.Contains("Query successful, but no results in the configured categories were returned from your indexer.", StringComparison.InvariantCultureIgnoreCase))
{
_logger.Warn(ex, "No Results in configured categories. See FAQ Entry: Prowlarr will not sync X Indexer to App");
break;
}
_logger.Error(ex, "Invalid Request");
break;
case HttpStatusCode.SeeOther:
case HttpStatusCode.TemporaryRedirect:
_logger.Warn(ex, "App returned redirect and is invalid. Check App URL");
break;
case HttpStatusCode.NotFound:
_logger.Warn(ex, "Remote indexer not found");
break;
default:
_logger.Error(ex, "Unexpected response status code: {0}", ex.Response.StatusCode);
break;
}
throw;
}
catch (JsonReaderException ex)
{
_logger.Error(ex, "Unable to parse JSON response from application");
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Unable to add or update indexer");
throw;
}
}
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 TResource Execute<TResource>(HttpRequest request)
where TResource : new()
{
var response = _httpClient.Execute(request);
if ((int)response.StatusCode >= 300)
{
throw new HttpException(response);
}
return Json.Deserialize<TResource>(response.Content);
}
}
}