refactor: Utilize DI for Flurl

The goal is to eliminate the need for a "global setup" step for HTTP
communication. This can instead be done in the composition root as part
of the factory to request FlurlClient objects.
pull/151/head
Robert Dailey 2 years ago
parent 2a79a50d50
commit d04b10f9d0

@ -5,7 +5,7 @@ using JetBrains.Annotations;
using Recyclarr.Config;
using Serilog;
using TrashLib.Config.Services;
using TrashLib.Extensions;
using TrashLib.Http;
using TrashLib.Services.CustomFormat;
using TrashLib.Services.Radarr;
using TrashLib.Services.Radarr.Config;

@ -3,15 +3,11 @@ using Autofac;
using CliFx.Attributes;
using CliFx.Exceptions;
using CliFx.Infrastructure;
using Common.Networking;
using Flurl.Http;
using Flurl.Http.Configuration;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Recyclarr.Migration;
using Serilog;
using TrashLib.Config.Settings;
using TrashLib.Extensions;
using TrashLib.Http;
using TrashLib.Repo;
using TrashLib.Repo.VersionControl;
using YamlDotNet.Core;
@ -76,7 +72,6 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
public override async Task Process(ILifetimeScope container)
{
var log = container.Resolve<ILogger>();
var settingsProvider = container.Resolve<ISettingsProvider>();
var repoUpdater = container.Resolve<IRepoUpdater>();
var migration = container.Resolve<IMigrationExecutor>();
@ -85,32 +80,6 @@ public abstract class ServiceCommand : BaseCommand, IServiceCommand
// Will throw if migration is required, otherwise just a warning is issued.
migration.CheckNeededMigrations();
SetupHttp(log, settingsProvider);
await repoUpdater.UpdateRepo();
}
private static void SetupHttp(ILogger log, ISettingsProvider settingsProvider)
{
FlurlHttp.Configure(settings =>
{
var jsonSettings = new JsonSerializerSettings
{
// This makes sure that null properties, such as maxSize and preferredSize in Radarr
// Quality Definitions, do not get written out to JSON request bodies.
NullValueHandling = NullValueHandling.Ignore
};
settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
FlurlLogging.SetupLogging(settings, log);
// ReSharper disable once InvertIf
if (!settingsProvider.Settings.EnableSslCertificateValidation)
{
log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting " +
"`enable_ssl_certificate_validation` is set to `false`");
settings.HttpClientFactory = new UntrustedCertClientFactory();
}
});
}
}

@ -5,7 +5,7 @@ using JetBrains.Annotations;
using Recyclarr.Config;
using Serilog;
using TrashLib.Config.Services;
using TrashLib.Extensions;
using TrashLib.Http;
using TrashLib.Services.CustomFormat;
using TrashLib.Services.Sonarr;
using TrashLib.Services.Sonarr.Config;

@ -13,6 +13,7 @@ using Recyclarr.Migration;
using TrashLib.Cache;
using TrashLib.Config;
using TrashLib.Config.Services;
using TrashLib.Http;
using TrashLib.Repo;
using TrashLib.Repo.VersionControl;
using TrashLib.Services.Common;
@ -52,7 +53,7 @@ public static class CompositionRoot
builder.RegisterModule<CacheAutofacModule>();
builder.RegisterType<CacheStoragePath>().As<ICacheStoragePath>();
builder.RegisterType<ServerInfo>().As<IServerInfo>();
builder.RegisterType<ServiceRequestBuilder>().As<IServiceRequestBuilder>();
builder.RegisterType<ProgressBar>();
ConfigurationRegistrations(builder);
@ -60,6 +61,8 @@ public static class CompositionRoot
builder.Register(_ => AutoMapperConfig.Setup()).SingleInstance();
builder.RegisterType<FlurlClientFactory>().As<IFlurlClientFactory>().SingleInstance();
extraRegistrations?.Invoke(builder);
return builder.Build();

@ -1,9 +0,0 @@
using Flurl;
namespace TrashLib.Config.Services;
public interface IServerInfo
{
Url BuildRequest();
string SanitizedBaseUrl { get; }
}

@ -0,0 +1,9 @@
using Flurl.Http;
namespace TrashLib.Config.Services;
public interface IServiceRequestBuilder
{
string SanitizedBaseUrl { get; }
IFlurlRequest Request(params object[] path);
}

@ -1,26 +0,0 @@
using Flurl;
using TrashLib.Extensions;
namespace TrashLib.Config.Services;
public class ServerInfo : IServerInfo
{
private readonly IServiceConfiguration _config;
public ServerInfo(IServiceConfiguration config)
{
_config = config;
}
public Url BuildRequest()
{
var apiKey = _config.ApiKey;
var baseUrl = _config.BaseUrl;
return baseUrl
.AppendPathSegment("api/v3")
.SetQueryParams(new {apikey = apiKey});
}
public string SanitizedBaseUrl => FlurlLogging.SanitizeUrl(_config.BaseUrl);
}

@ -0,0 +1,25 @@
using Flurl.Http;
using TrashLib.Http;
namespace TrashLib.Config.Services;
public class ServiceRequestBuilder : IServiceRequestBuilder
{
private readonly IServiceConfiguration _config;
private readonly IFlurlClientFactory _clientFactory;
public ServiceRequestBuilder(IServiceConfiguration config, IFlurlClientFactory clientFactory)
{
_config = config;
_clientFactory = clientFactory;
}
public IFlurlRequest Request(params object[] path)
{
var client = _clientFactory.Get(_config.BaseUrl);
return client.Request(new[] {"api", "v3"}.Concat(path).ToArray())
.SetQueryParams(new {apikey = _config.ApiKey});
}
public string SanitizedBaseUrl => FlurlLogging.SanitizeUrl(_config.BaseUrl);
}

@ -0,0 +1,55 @@
using Common.Networking;
using Flurl.Http;
using Flurl.Http.Configuration;
using Newtonsoft.Json;
using Serilog;
using TrashLib.Config.Settings;
namespace TrashLib.Http;
public class FlurlClientFactory : IFlurlClientFactory
{
private readonly ILogger _log;
private readonly ISettingsProvider _settingsProvider;
private readonly PerBaseUrlFlurlClientFactory _factory;
public FlurlClientFactory(ILogger log, ISettingsProvider settingsProvider)
{
_log = log;
_settingsProvider = settingsProvider;
_factory = new PerBaseUrlFlurlClientFactory();
}
public IFlurlClient Get(string baseUrl)
{
var client = _factory.Get(baseUrl);
client.Settings = GetClientSettings();
return client;
}
private ClientFlurlHttpSettings GetClientSettings()
{
var settings = new ClientFlurlHttpSettings
{
JsonSerializer = new NewtonsoftJsonSerializer(new JsonSerializerSettings
{
// This makes sure that null properties, such as maxSize and preferredSize in Radarr
// Quality Definitions, do not get written out to JSON request bodies.
NullValueHandling = NullValueHandling.Ignore
})
};
FlurlLogging.SetupLogging(settings, _log);
// ReSharper disable once InvertIf
if (!_settingsProvider.Settings.EnableSslCertificateValidation)
{
_log.Warning(
"Security Risk: Certificate validation is being DISABLED because setting " +
"`enable_ssl_certificate_validation` is set to `false`");
settings.HttpClientFactory = new UntrustedCertClientFactory();
}
return settings;
}
}

@ -1,7 +1,7 @@
using System.Text.RegularExpressions;
using Flurl.Http;
namespace TrashLib.Extensions;
namespace TrashLib.Http;
public static class FlurlExtensions
{

@ -3,7 +3,7 @@ using Flurl.Http.Configuration;
using Newtonsoft.Json;
using Serilog;
namespace TrashLib.Extensions;
namespace TrashLib.Http;
public static class FlurlLogging
{

@ -0,0 +1,8 @@
using Flurl.Http;
namespace TrashLib.Http;
public interface IFlurlClientFactory
{
IFlurlClient Get(string baseUrl);
}

@ -1,4 +1,3 @@
using Flurl;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config.Services;
@ -8,24 +7,22 @@ namespace TrashLib.Services.CustomFormat.Api;
internal class CustomFormatService : ICustomFormatService
{
private readonly IServerInfo _serverInfo;
private readonly IServiceRequestBuilder _service;
public CustomFormatService(IServerInfo serverInfo)
public CustomFormatService(IServiceRequestBuilder service)
{
_serverInfo = serverInfo;
_service = service;
}
public async Task<List<JObject>> GetCustomFormats()
{
return await BuildRequest()
.AppendPathSegment("customformat")
return await _service.Request("customformat")
.GetJsonAsync<List<JObject>>();
}
public async Task CreateCustomFormat(ProcessedCustomFormatData cf)
{
var response = await BuildRequest()
.AppendPathSegment("customformat")
var response = await _service.Request("customformat")
.PostJsonAsync(cf.Json)
.ReceiveJson<JObject>();
@ -37,18 +34,14 @@ internal class CustomFormatService : ICustomFormatService
public async Task UpdateCustomFormat(ProcessedCustomFormatData cf)
{
await BuildRequest()
.AppendPathSegment($"customformat/{cf.GetCustomFormatId()}")
await _service.Request("customformat", cf.GetCustomFormatId())
.PutJsonAsync(cf.Json)
.ReceiveJson<JObject>();
}
public async Task DeleteCustomFormat(int customFormatId)
{
await BuildRequest()
.AppendPathSegment($"customformat/{customFormatId}")
await _service.Request("customformat", customFormatId)
.DeleteAsync();
}
private Url BuildRequest() => _serverInfo.BuildRequest();
}

@ -1,4 +1,3 @@
using Flurl;
using Flurl.Http;
using Newtonsoft.Json.Linq;
using TrashLib.Config.Services;
@ -7,27 +6,23 @@ namespace TrashLib.Services.CustomFormat.Api;
internal class QualityProfileService : IQualityProfileService
{
private readonly IServerInfo _serverInfo;
private readonly IServiceRequestBuilder _service;
public QualityProfileService(IServerInfo serverInfo)
public QualityProfileService(IServiceRequestBuilder service)
{
_serverInfo = serverInfo;
_service = service;
}
public async Task<List<JObject>> GetQualityProfiles()
{
return await BuildRequest()
.AppendPathSegment("qualityprofile")
return await _service.Request("qualityprofile")
.GetJsonAsync<List<JObject>>();
}
public async Task<JObject> UpdateQualityProfile(JObject profileJson, int id)
{
return await BuildRequest()
.AppendPathSegment($"qualityprofile/{id}")
return await _service.Request("qualityprofile", id)
.PutJsonAsync(profileJson)
.ReceiveJson<JObject>();
}
private Url BuildRequest() => _serverInfo.BuildRequest();
}

@ -14,7 +14,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
private readonly IGuideProcessor _guideProcessor;
private readonly IPersistenceProcessor _persistenceProcessor;
private readonly IConsole _console;
private readonly IServerInfo _serverInfo;
private readonly IServiceRequestBuilder _service;
private readonly ILogger _log;
public CustomFormatUpdater(
@ -23,14 +23,14 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
IGuideProcessor guideProcessor,
IPersistenceProcessor persistenceProcessor,
IConsole console,
IServerInfo serverInfo)
IServiceRequestBuilder service)
{
_log = log;
_cache = cache;
_guideProcessor = guideProcessor;
_persistenceProcessor = persistenceProcessor;
_console = console;
_serverInfo = serverInfo;
_service = service;
}
public async Task Process(bool isPreview, IEnumerable<CustomFormatConfig> configs, IGuideService guideService)
@ -141,8 +141,8 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
if (_guideProcessor.CustomFormatsNotInGuide.Count > 0)
{
_log.Warning("The Custom Formats below do not exist in the guide and will " +
"be skipped. Trash IDs must match what is listed in the output when using the " +
"`--list-custom-formats` option");
"be skipped. Trash IDs must match what is listed in the output when using the " +
"`--list-custom-formats` option");
_log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide);
_console.Output.WriteLine("");
@ -165,7 +165,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
if (_guideProcessor.ConfigData.Count == 0)
{
_log.Error("Guide processing yielded no custom formats for configured instance host {BaseUrl}",
_serverInfo.SanitizedBaseUrl);
_service.SanitizedBaseUrl);
return false;
}

@ -1,4 +1,3 @@
using Flurl;
using Flurl.Http;
using TrashLib.Config.Services;
using TrashLib.Services.Radarr.QualityDefinition.Api.Objects;
@ -7,28 +6,24 @@ namespace TrashLib.Services.Radarr.QualityDefinition.Api;
internal class QualityDefinitionService : IQualityDefinitionService
{
private readonly IServerInfo _serverInfo;
private readonly IServiceRequestBuilder _service;
public QualityDefinitionService(IServerInfo serverInfo)
public QualityDefinitionService(IServiceRequestBuilder service)
{
_serverInfo = serverInfo;
_service = service;
}
public async Task<List<RadarrQualityDefinitionItem>> GetQualityDefinition()
{
return await BuildRequest()
.AppendPathSegment("qualitydefinition")
return await _service.Request("qualitydefinition")
.GetJsonAsync<List<RadarrQualityDefinitionItem>>();
}
public async Task<IList<RadarrQualityDefinitionItem>> UpdateQualityDefinition(
IList<RadarrQualityDefinitionItem> newQuality)
{
return await BuildRequest()
.AppendPathSegment("qualityDefinition/update")
return await _service.Request("qualityDefinition", "update")
.PutJsonAsync(newQuality)
.ReceiveJson<List<RadarrQualityDefinitionItem>>();
}
private Url BuildRequest() => _serverInfo.BuildRequest();
}

@ -8,21 +8,20 @@ namespace TrashLib.Services.Sonarr.Api;
public class ReleaseProfileApiService : IReleaseProfileApiService
{
private readonly ISonarrReleaseProfileCompatibilityHandler _profileHandler;
private readonly IServerInfo _serverInfo;
private readonly IServiceRequestBuilder _service;
public ReleaseProfileApiService(
ISonarrReleaseProfileCompatibilityHandler profileHandler,
IServerInfo serverInfo)
IServiceRequestBuilder service)
{
_profileHandler = profileHandler;
_serverInfo = serverInfo;
_service = service;
}
public async Task UpdateReleaseProfile(SonarrReleaseProfile profile)
{
var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(profile);
await _serverInfo.BuildRequest()
.AppendPathSegment($"releaseprofile/{profile.Id}")
await _service.Request("releaseprofile", profile.Id)
.PutJsonAsync(profileToSend);
}
@ -30,8 +29,7 @@ public class ReleaseProfileApiService : IReleaseProfileApiService
{
var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(profile);
var response = await _serverInfo.BuildRequest()
.AppendPathSegment("releaseprofile")
var response = await _service.Request("releaseprofile")
.PostJsonAsync(profileToSend)
.ReceiveJson<JObject>();
@ -40,8 +38,7 @@ public class ReleaseProfileApiService : IReleaseProfileApiService
public async Task<IList<SonarrReleaseProfile>> GetReleaseProfiles()
{
var response = await _serverInfo.BuildRequest()
.AppendPathSegment("releaseprofile")
var response = await _service.Request("releaseprofile")
.GetJsonAsync<List<JObject>>();
return response
@ -51,8 +48,7 @@ public class ReleaseProfileApiService : IReleaseProfileApiService
public async Task DeleteReleaseProfile(int releaseProfileId)
{
await _serverInfo.BuildRequest()
.AppendPathSegment($"releaseprofile/{releaseProfileId}")
await _service.Request("releaseprofile", releaseProfileId)
.DeleteAsync();
}
}

@ -1,4 +1,3 @@
using Flurl;
using Flurl.Http;
using TrashLib.Config.Services;
using TrashLib.Services.Sonarr.Api.Objects;
@ -7,43 +6,37 @@ namespace TrashLib.Services.Sonarr.Api;
public class SonarrApi : ISonarrApi
{
private readonly IServerInfo _serverInfo;
private readonly IServiceRequestBuilder _service;
public SonarrApi(IServerInfo serverInfo)
public SonarrApi(IServiceRequestBuilder service)
{
_serverInfo = serverInfo;
_service = service;
}
public async Task<IList<SonarrTag>> GetTags()
{
return await BaseUrl()
.AppendPathSegment("tag")
return await _service.Request("tag")
.GetJsonAsync<List<SonarrTag>>();
}
public async Task<SonarrTag> CreateTag(string tag)
{
return await BaseUrl()
.AppendPathSegment("tag")
return await _service.Request("tag")
.PostJsonAsync(new {label = tag})
.ReceiveJson<SonarrTag>();
}
public async Task<IReadOnlyCollection<SonarrQualityDefinitionItem>> GetQualityDefinition()
{
return await BaseUrl()
.AppendPathSegment("qualitydefinition")
return await _service.Request("qualitydefinition")
.GetJsonAsync<List<SonarrQualityDefinitionItem>>();
}
public async Task<IList<SonarrQualityDefinitionItem>> UpdateQualityDefinition(
IReadOnlyCollection<SonarrQualityDefinitionItem> newQuality)
{
return await BaseUrl()
.AppendPathSegment("qualityDefinition/update")
return await _service.Request("qualityDefinition", "update")
.PutJsonAsync(newQuality)
.ReceiveJson<List<SonarrQualityDefinitionItem>>();
}
private Url BaseUrl() => _serverInfo.BuildRequest();
}

@ -1,4 +1,3 @@
using Flurl;
using Flurl.Http;
using TrashLib.Config.Services;
using TrashLib.Services.System.Dto;
@ -7,19 +6,16 @@ namespace TrashLib.Services.System;
public class SystemApiService : ISystemApiService
{
private readonly IServerInfo _serverInfo;
private readonly IServiceRequestBuilder _service;
public SystemApiService(IServerInfo serverInfo)
public SystemApiService(IServiceRequestBuilder service)
{
_serverInfo = serverInfo;
_service = service;
}
public async Task<SystemStatus> GetStatus()
{
return await BaseUrl()
.AppendPathSegment("system/status")
return await _service.Request("system", "status")
.GetJsonAsync<SystemStatus>();
}
private Url BaseUrl() => _serverInfo.BuildRequest();
}

Loading…
Cancel
Save