fix: Signal interrupt support for all API calls

pull/286/head
Robert Dailey 4 months ago
parent f420a3694c
commit e11753d7ee

@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Sonarr or Radarr UI. Recyclarr now detects this situation and automatically repairs the quality
profile by re-adding these missing qualities for users. See [this issue][9738].
### Fixed
- Signal interrupt support for all API calls. Now when you press CTRL+C to gracefully exit/cancel
Recyclarr, it will bail out of any ongoing API calls.
[9738]: https://github.com/Radarr/Radarr/issues/9738
## [7.0.0] - 2024-06-27

@ -49,7 +49,7 @@ public class DeleteCustomFormatsCommand(
{
try
{
await processor.Process(settings);
await processor.Process(settings, settings.CancellationToken);
}
catch (Exception e)
{

@ -53,6 +53,6 @@ public class SyncCommand(IMigrationExecutor migration, IMultiRepoUpdater repoUpd
await repoUpdater.UpdateAllRepositories(settings.CancellationToken);
return (int) await syncProcessor.ProcessConfigs(settings);
return (int) await syncProcessor.ProcessConfigs(settings, settings.CancellationToken);
}
}

@ -3,21 +3,19 @@ namespace Recyclarr.Cli.Console.Helpers;
// Taken from: https://github.com/spectreconsole/spectre.console/issues/701#issuecomment-1081834778
internal sealed class ConsoleAppCancellationTokenSource : IDisposable
{
public void Dispose()
{
_cts.Dispose();
}
private readonly ILogger _log;
private readonly CancellationTokenSource _cts = new();
public CancellationToken Token => _cts.Token;
public ConsoleAppCancellationTokenSource()
public ConsoleAppCancellationTokenSource(ILogger log)
{
_log = log;
System.Console.CancelKeyPress += OnCancelKeyPress;
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
using var _ = _cts.Token.Register(() =>
_cts.Token.Register(() =>
{
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
System.Console.CancelKeyPress -= OnCancelKeyPress;
@ -26,6 +24,8 @@ internal sealed class ConsoleAppCancellationTokenSource : IDisposable
private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e)
{
_log.Information("Exiting due to signal interrupt");
// NOTE: cancel event, don't terminate the process
e.Cancel = true;
@ -43,4 +43,9 @@ internal sealed class ConsoleAppCancellationTokenSource : IDisposable
_cts.Cancel();
}
public void Dispose()
{
_cts.Dispose();
}
}

@ -7,15 +7,13 @@ using Spectre.Console.Cli;
namespace Recyclarr.Cli.Console.Interceptors;
internal sealed class BaseCommandSetupInterceptor(LoggingLevelSwitch loggingLevelSwitch, IAppDataSetup appDataSetup)
internal sealed class BaseCommandSetupInterceptor(
ILogger log,
LoggingLevelSwitch loggingLevelSwitch,
IAppDataSetup appDataSetup)
: ICommandInterceptor, IDisposable
{
public void Dispose()
{
_ct.Dispose();
}
private readonly ConsoleAppCancellationTokenSource _ct = new();
private readonly ConsoleAppCancellationTokenSource _ct = new(log);
public void Intercept(CommandContext context, CommandSettings settings)
{
@ -46,4 +44,9 @@ internal sealed class BaseCommandSetupInterceptor(LoggingLevelSwitch loggingLeve
_ => LogEventLevel.Information
};
}
public void Dispose()
{
_ct.Dispose();
}
}

@ -7,9 +7,9 @@ namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
public class CustomFormatApiFetchPhase(ICustomFormatApiService api)
: IApiFetchPipelinePhase<CustomFormatPipelineContext>
{
public async Task Execute(CustomFormatPipelineContext context)
public async Task Execute(CustomFormatPipelineContext context, CancellationToken ct)
{
var result = await api.GetCustomFormats();
var result = await api.GetCustomFormats(ct);
context.ApiFetchOutput.AddRange(result);
context.Cache.RemoveStale(result);
}

@ -7,13 +7,13 @@ namespace Recyclarr.Cli.Pipelines.CustomFormat.PipelinePhases;
public class CustomFormatApiPersistencePhase(ICustomFormatApiService api, ICustomFormatCachePersister cachePersister)
: IApiPersistencePipelinePhase<CustomFormatPipelineContext>
{
public async Task Execute(CustomFormatPipelineContext context)
public async Task Execute(CustomFormatPipelineContext context, CancellationToken ct)
{
var transactions = context.TransactionOutput;
foreach (var cf in transactions.NewCustomFormats)
{
var response = await api.CreateCustomFormat(cf);
var response = await api.CreateCustomFormat(cf, ct);
if (response is not null)
{
cf.Id = response.Id;
@ -22,12 +22,12 @@ public class CustomFormatApiPersistencePhase(ICustomFormatApiService api, ICusto
foreach (var dto in transactions.UpdatedCustomFormats)
{
await api.UpdateCustomFormat(dto);
await api.UpdateCustomFormat(dto, ct);
}
foreach (var map in transactions.DeletedCustomFormats)
{
await api.DeleteCustomFormat(map.CustomFormatId);
await api.DeleteCustomFormat(map.CustomFormatId, ct);
}
context.Cache.Update(transactions);

@ -10,7 +10,7 @@ public class GenericSyncPipeline<TContext>(
) : ISyncPipeline
where TContext : IPipelineContext, new()
{
public async Task Execute(ISyncSettings settings)
public async Task Execute(ISyncSettings settings, CancellationToken ct)
{
var context = new TContext();
if (!context.SupportedServiceTypes.Contains(config.ServiceType))
@ -26,7 +26,7 @@ public class GenericSyncPipeline<TContext>(
return;
}
await phases.ApiFetchPhase.Execute(context);
await phases.ApiFetchPhase.Execute(context, ct);
phases.TransactionPhase.Execute(context);
phases.LogPhase.LogTransactionNotices(context);
@ -37,7 +37,7 @@ public class GenericSyncPipeline<TContext>(
return;
}
await phases.ApiPersistencePhase.Execute(context);
await phases.ApiPersistencePhase.Execute(context, ct);
phases.LogPhase.LogPersistenceResults(context);
}
}

@ -3,5 +3,5 @@ namespace Recyclarr.Cli.Pipelines.Generic;
public interface IApiFetchPipelinePhase<in TContext>
where TContext : IPipelineContext
{
Task Execute(TContext context);
Task Execute(TContext context, CancellationToken ct);
}

@ -3,5 +3,5 @@ namespace Recyclarr.Cli.Pipelines.Generic;
public interface IApiPersistencePipelinePhase<in TContext>
where TContext : IPipelineContext
{
Task Execute(TContext context);
Task Execute(TContext context, CancellationToken ct);
}

@ -2,7 +2,9 @@ using Recyclarr.Cli.Console.Settings;
namespace Recyclarr.Cli.Pipelines;
// This interface is valuable because it allows having a collection of generic sync pipelines without needing to be
// aware of the generic parameters.
public interface ISyncPipeline
{
public Task Execute(ISyncSettings settings);
public Task Execute(ISyncSettings settings, CancellationToken ct);
}

@ -5,8 +5,8 @@ namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
public class MediaNamingApiFetchPhase(IMediaNamingApiService api) : IApiFetchPipelinePhase<MediaNamingPipelineContext>
{
public async Task Execute(MediaNamingPipelineContext context)
public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct)
{
context.ApiFetchOutput = await api.GetNaming();
context.ApiFetchOutput = await api.GetNaming(ct);
}
}

@ -6,8 +6,8 @@ namespace Recyclarr.Cli.Pipelines.MediaNaming.PipelinePhases;
public class MediaNamingApiPersistencePhase(IMediaNamingApiService api)
: IApiPersistencePipelinePhase<MediaNamingPipelineContext>
{
public async Task Execute(MediaNamingPipelineContext context)
public async Task Execute(MediaNamingPipelineContext context, CancellationToken ct)
{
await api.UpdateNaming(context.TransactionOutput);
await api.UpdateNaming(context.TransactionOutput, ct);
}
}

@ -8,10 +8,10 @@ public record QualityProfileServiceData(IReadOnlyList<QualityProfileDto> Profile
public class QualityProfileApiFetchPhase(IQualityProfileApiService api)
: IApiFetchPipelinePhase<QualityProfilePipelineContext>
{
public async Task Execute(QualityProfilePipelineContext context)
public async Task Execute(QualityProfilePipelineContext context, CancellationToken ct)
{
var profiles = await api.GetQualityProfiles();
var schema = await api.GetSchema();
var profiles = await api.GetQualityProfiles(ct);
var schema = await api.GetSchema(ct);
context.ApiFetchOutput = new QualityProfileServiceData(profiles.AsReadOnly(), schema);
}
}

@ -7,7 +7,7 @@ namespace Recyclarr.Cli.Pipelines.QualityProfile.PipelinePhases;
public class QualityProfileApiPersistencePhase(IQualityProfileApiService api)
: IApiPersistencePipelinePhase<QualityProfilePipelineContext>
{
public async Task Execute(QualityProfilePipelineContext context)
public async Task Execute(QualityProfilePipelineContext context, CancellationToken ct)
{
var changedProfiles = context.TransactionOutput.ChangedProfiles;
foreach (var profile in changedProfiles.Select(x => x.Profile))
@ -17,11 +17,11 @@ public class QualityProfileApiPersistencePhase(IQualityProfileApiService api)
switch (profile.UpdateReason)
{
case QualityProfileUpdateReason.New:
await api.CreateQualityProfile(dto);
await api.CreateQualityProfile(dto, ct);
break;
case QualityProfileUpdateReason.Changed:
await api.UpdateQualityProfile(dto);
await api.UpdateQualityProfile(dto, ct);
break;
default:

@ -6,8 +6,8 @@ namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeApiFetchPhase(IQualityDefinitionApiService api)
: IApiFetchPipelinePhase<QualitySizePipelineContext>
{
public async Task Execute(QualitySizePipelineContext context)
public async Task Execute(QualitySizePipelineContext context, CancellationToken ct)
{
context.ApiFetchOutput = await api.GetQualityDefinition();
context.ApiFetchOutput = await api.GetQualityDefinition(ct);
}
}

@ -6,8 +6,8 @@ namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeApiPersistencePhase(IQualityDefinitionApiService api)
: IApiPersistencePipelinePhase<QualitySizePipelineContext>
{
public async Task Execute(QualitySizePipelineContext context)
public async Task Execute(QualitySizePipelineContext context, CancellationToken ct)
{
await api.UpdateQualityDefinition(context.TransactionOutput);
await api.UpdateQualityDefinition(context.TransactionOutput, ct);
}
}

@ -24,11 +24,11 @@ public class DeleteCustomFormatsProcessor(
ConfigurationScopeFactory scopeFactory)
: IDeleteCustomFormatsProcessor
{
public async Task Process(IDeleteCustomFormatSettings settings)
public async Task Process(IDeleteCustomFormatSettings settings, CancellationToken ct)
{
using var scope = scopeFactory.Start<CustomFormatConfigurationScope>(GetTargetConfig(settings));
var cfs = await ObtainCustomFormats(scope.CustomFormatApi);
var cfs = await ObtainCustomFormats(scope.CustomFormatApi, ct);
if (!settings.All)
{
@ -90,13 +90,13 @@ public class DeleteCustomFormatsProcessor(
});
}
private async Task<IList<CustomFormatData>> ObtainCustomFormats(ICustomFormatApiService api)
private async Task<IList<CustomFormatData>> ObtainCustomFormats(ICustomFormatApiService api, CancellationToken ct)
{
IList<CustomFormatData> cfs = new List<CustomFormatData>();
await console.Status().StartAsync("Obtaining custom formats...", async _ =>
{
cfs = await api.GetCustomFormats();
cfs = await api.GetCustomFormats(ct);
});
return cfs;

@ -4,5 +4,5 @@ namespace Recyclarr.Cli.Processors.Delete;
public interface IDeleteCustomFormatsProcessor
{
Task Process(IDeleteCustomFormatSettings settings);
Task Process(IDeleteCustomFormatSettings settings, CancellationToken ct);
}

@ -8,20 +8,24 @@ public class FlurlHttpExceptionHandler(ILogger log) : IFlurlHttpExceptionHandler
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public async Task ProcessServiceErrorMessages(IServiceErrorMessageExtractor extractor)
{
switch (extractor)
if (extractor.ErrorMessage.Contains("task was canceled"))
{
case {HttpStatusCode: 401}:
log.Error("Reason: Recyclarr is unauthorized to talk to the service. Is your `api_key` correct?");
break;
log.Error("Reason: User canceled the operation");
return;
}
case {HttpStatusCode: null}:
log.Error("Reason: Problem connecting to service. Is your `base_url` correct?");
break;
switch (extractor.HttpStatusCode)
{
case 401:
log.Error("Reason: Recyclarr is unauthorized to talk to the service. Is your `api_key` correct?");
return;
default:
ProcessBody(await extractor.GetErrorMessage());
break;
case null:
log.Error("Reason: Problem connecting to the service. Is your `base_url` correct?");
return;
}
ProcessBody(await extractor.GetErrorMessage());
}
private void ProcessBody(string responseBody)

@ -5,4 +5,5 @@ public interface IServiceErrorMessageExtractor
Task<string> GetErrorMessage();
int? HttpStatusCode { get; }
HttpRequestError? HttpError { get; }
string ErrorMessage { get; }
}

@ -23,4 +23,6 @@ public class ServiceErrorMessageExtractor(FlurlHttpException e) : IServiceErrorM
}
public int? HttpStatusCode => e.StatusCode;
public string ErrorMessage => e.Message;
}

@ -4,5 +4,5 @@ namespace Recyclarr.Cli.Processors.Sync;
public interface ISyncProcessor
{
Task<ExitStatus> ProcessConfigs(ISyncSettings settings);
Task<ExitStatus> ProcessConfigs(ISyncSettings settings, CancellationToken ct);
}

@ -14,11 +14,11 @@ public class SyncPipelineExecutor(
ServiceAgnosticCapabilityEnforcer enforcer,
IServiceConfiguration config)
{
public async Task Process(ISyncSettings settings)
public async Task Process(ISyncSettings settings, CancellationToken ct)
{
PrintProcessingHeader();
await enforcer.Check(config);
await enforcer.Check(config, ct);
foreach (var cache in caches)
{
@ -28,7 +28,7 @@ public class SyncPipelineExecutor(
foreach (var pipeline in pipelines)
{
log.Debug("Executing Pipeline: {Pipeline}", pipeline.GetType().Name);
await pipeline.Execute(settings);
await pipeline.Execute(settings, ct);
}
log.Information("Completed at {Date}", DateTime.Now);

@ -19,7 +19,7 @@ public class SyncProcessor(
ConsoleExceptionHandler exceptionHandler)
: ISyncProcessor
{
public async Task<ExitStatus> ProcessConfigs(ISyncSettings settings)
public async Task<ExitStatus> ProcessConfigs(ISyncSettings settings, CancellationToken ct)
{
bool failureDetected;
try
@ -31,7 +31,7 @@ public class SyncProcessor(
Service = settings.Service
});
failureDetected = await ProcessService(settings, configs);
failureDetected = await ProcessService(settings, configs, ct);
}
catch (Exception e)
{
@ -47,7 +47,10 @@ public class SyncProcessor(
return failureDetected ? ExitStatus.Failed : ExitStatus.Succeeded;
}
private async Task<bool> ProcessService(ISyncSettings settings, IEnumerable<IServiceConfiguration> configs)
private async Task<bool> ProcessService(
ISyncSettings settings,
IEnumerable<IServiceConfiguration> configs,
CancellationToken ct)
{
var failureDetected = false;
@ -56,7 +59,7 @@ public class SyncProcessor(
try
{
using var scope = configScopeFactory.Start<SyncBasedConfigurationScope>(config);
await scope.Pipelines.Process(settings);
await scope.Pipelines.Process(settings, ct);
}
catch (Exception e)
{

@ -2,5 +2,5 @@ namespace Recyclarr.Compatibility;
public interface IServiceInformation
{
public Task<Version> GetVersion();
public Task<Version> GetVersion(CancellationToken ct);
}

@ -2,5 +2,5 @@ namespace Recyclarr.Compatibility.Radarr;
public interface IRadarrCapabilityFetcher
{
Task<RadarrCapabilities> GetCapabilities();
Task<RadarrCapabilities> GetCapabilities(CancellationToken ct);
}

@ -2,9 +2,9 @@ namespace Recyclarr.Compatibility.Radarr;
public class RadarrCapabilityEnforcer(IRadarrCapabilityFetcher capabilityFetcher)
{
public async Task Check()
public async Task Check(CancellationToken ct)
{
_ = await capabilityFetcher.GetCapabilities();
_ = await capabilityFetcher.GetCapabilities(ct);
// For the future: Add more capability checks here as needed
}

@ -8,16 +8,16 @@ public class ServiceAgnosticCapabilityEnforcer(
SonarrCapabilityEnforcer sonarrEnforcer,
RadarrCapabilityEnforcer radarrEnforcer)
{
public async Task Check(IServiceConfiguration config)
public async Task Check(IServiceConfiguration config, CancellationToken ct)
{
switch (config)
{
case SonarrConfiguration:
await sonarrEnforcer.Check();
await sonarrEnforcer.Check(ct);
break;
case RadarrConfiguration:
await radarrEnforcer.Check();
await radarrEnforcer.Check(ct);
break;
}
}

@ -3,9 +3,9 @@ namespace Recyclarr.Compatibility;
public abstract class ServiceCapabilityFetcher<T>(IServiceInformation info)
where T : class
{
public async Task<T> GetCapabilities()
public async Task<T> GetCapabilities(CancellationToken ct)
{
var version = await info.GetVersion();
var version = await info.GetVersion(ct);
return BuildCapabilitiesObject(version);
}

@ -6,11 +6,11 @@ namespace Recyclarr.Compatibility;
public class ServiceInformation(ISystemApiService api, ILogger log) : IServiceInformation
{
public async Task<Version> GetVersion()
public async Task<Version> GetVersion(CancellationToken ct)
{
try
{
var status = await api.GetStatus();
var status = await api.GetStatus(ct);
log.Debug("{Service} Version: {Version}", status.AppName, status.Version);
return new Version(status.Version);
}

@ -2,5 +2,5 @@ namespace Recyclarr.Compatibility.Sonarr;
public interface ISonarrCapabilityFetcher
{
Task<SonarrCapabilities> GetCapabilities();
Task<SonarrCapabilities> GetCapabilities(CancellationToken ct);
}

@ -2,9 +2,9 @@ namespace Recyclarr.Compatibility.Sonarr;
public class SonarrCapabilityEnforcer(ISonarrCapabilityFetcher capabilityFetcher)
{
public async Task Check()
public async Task Check(CancellationToken ct)
{
var capabilities = await capabilityFetcher.GetCapabilities();
var capabilities = await capabilityFetcher.GetCapabilities(ct);
if (capabilities.Version < SonarrCapabilities.MinimumVersion)
{

@ -10,28 +10,28 @@ public class CustomFormatApiService(IServarrRequestBuilder service) : ICustomFor
return service.Request(["customformat", ..path]);
}
public async Task<IList<CustomFormatData>> GetCustomFormats()
public async Task<IList<CustomFormatData>> GetCustomFormats(CancellationToken ct)
{
return await Request()
.GetJsonAsync<IList<CustomFormatData>>();
.GetJsonAsync<IList<CustomFormatData>>(cancellationToken: ct);
}
public async Task<CustomFormatData?> CreateCustomFormat(CustomFormatData cf)
public async Task<CustomFormatData?> CreateCustomFormat(CustomFormatData cf, CancellationToken ct)
{
return await Request()
.PostJsonAsync(cf)
.PostJsonAsync(cf, cancellationToken: ct)
.ReceiveJson<CustomFormatData>();
}
public async Task UpdateCustomFormat(CustomFormatData cf)
public async Task UpdateCustomFormat(CustomFormatData cf, CancellationToken ct)
{
await Request(cf.Id)
.PutJsonAsync(cf);
.PutJsonAsync(cf, cancellationToken: ct);
}
public async Task DeleteCustomFormat(int customFormatId, CancellationToken cancellationToken = default)
public async Task DeleteCustomFormat(int customFormatId, CancellationToken ct)
{
await Request(customFormatId)
.DeleteAsync(cancellationToken: cancellationToken);
.DeleteAsync(cancellationToken: ct);
}
}

@ -4,8 +4,8 @@ namespace Recyclarr.ServarrApi.CustomFormat;
public interface ICustomFormatApiService
{
Task<IList<CustomFormatData>> GetCustomFormats();
Task<CustomFormatData?> CreateCustomFormat(CustomFormatData cf);
Task UpdateCustomFormat(CustomFormatData cf);
Task DeleteCustomFormat(int customFormatId, CancellationToken cancellationToken = default);
Task<IList<CustomFormatData>> GetCustomFormats(CancellationToken ct);
Task<CustomFormatData?> CreateCustomFormat(CustomFormatData cf, CancellationToken ct);
Task UpdateCustomFormat(CustomFormatData cf, CancellationToken ct);
Task DeleteCustomFormat(int customFormatId, CancellationToken ct);
}

@ -2,6 +2,6 @@ namespace Recyclarr.ServarrApi.MediaNaming;
public interface IMediaNamingApiService
{
Task<MediaNamingDto> GetNaming();
Task UpdateNaming(MediaNamingDto dto);
Task<MediaNamingDto> GetNaming(CancellationToken ct);
Task UpdateNaming(MediaNamingDto dto, CancellationToken ct);
}

@ -7,9 +7,16 @@ namespace Recyclarr.ServarrApi.MediaNaming;
public class MediaNamingApiService(IServarrRequestBuilder service, IServiceConfiguration config)
: IMediaNamingApiService
{
public async Task<MediaNamingDto> GetNaming()
private IFlurlRequest Request(params object[] path)
{
var response = await service.Request("config", "naming").GetAsync();
return service.Request(["config", "naming", ..path]);
}
public async Task<MediaNamingDto> GetNaming(CancellationToken ct)
{
var response = await Request()
.GetAsync(cancellationToken: ct);
return config.ServiceType switch
{
SupportedServices.Radarr => await response.GetJsonAsync<RadarrMediaNamingDto>(),
@ -18,9 +25,9 @@ public class MediaNamingApiService(IServarrRequestBuilder service, IServiceConfi
};
}
public async Task UpdateNaming(MediaNamingDto dto)
public async Task UpdateNaming(MediaNamingDto dto, CancellationToken ct)
{
await service.Request("config", "naming")
.PutJsonAsync(dto);
await Request()
.PutJsonAsync(dto, cancellationToken: ct);
}
}

@ -2,6 +2,9 @@ namespace Recyclarr.ServarrApi.QualityDefinition;
public interface IQualityDefinitionApiService
{
Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition();
Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(IList<ServiceQualityDefinitionItem> newQuality);
Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition(CancellationToken ct);
Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(
IList<ServiceQualityDefinitionItem> newQuality,
CancellationToken ct);
}

@ -4,17 +4,23 @@ namespace Recyclarr.ServarrApi.QualityDefinition;
internal class QualityDefinitionApiService(IServarrRequestBuilder service) : IQualityDefinitionApiService
{
public async Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition()
private IFlurlRequest Request(params object[] path)
{
return await service.Request("qualitydefinition")
.GetJsonAsync<List<ServiceQualityDefinitionItem>>();
return service.Request(["qualitydefinition", ..path]);
}
public async Task<IList<ServiceQualityDefinitionItem>> GetQualityDefinition(CancellationToken ct)
{
return await Request()
.GetJsonAsync<List<ServiceQualityDefinitionItem>>(cancellationToken: ct);
}
public async Task<IList<ServiceQualityDefinitionItem>> UpdateQualityDefinition(
IList<ServiceQualityDefinitionItem> newQuality)
IList<ServiceQualityDefinitionItem> newQuality,
CancellationToken ct)
{
return await service.Request("qualityDefinition", "update")
.PutJsonAsync(newQuality)
return await Request("update")
.PutJsonAsync(newQuality, cancellationToken: ct)
.ReceiveJson<List<ServiceQualityDefinitionItem>>();
}
}

@ -2,8 +2,8 @@ namespace Recyclarr.ServarrApi.QualityProfile;
public interface IQualityProfileApiService
{
Task<IList<QualityProfileDto>> GetQualityProfiles();
Task UpdateQualityProfile(QualityProfileDto profile);
Task<QualityProfileDto> GetSchema();
Task CreateQualityProfile(QualityProfileDto profile);
Task<IList<QualityProfileDto>> GetQualityProfiles(CancellationToken ct);
Task UpdateQualityProfile(QualityProfileDto profile, CancellationToken ct);
Task<QualityProfileDto> GetSchema(CancellationToken ct);
Task CreateQualityProfile(QualityProfileDto profile, CancellationToken ct);
}

@ -9,23 +9,23 @@ internal class QualityProfileApiService(IServarrRequestBuilder service) : IQuali
return service.Request(["qualityprofile", ..path]);
}
public async Task<IList<QualityProfileDto>> GetQualityProfiles()
public async Task<IList<QualityProfileDto>> GetQualityProfiles(CancellationToken ct)
{
var response = await Request()
.GetJsonAsync<IList<QualityProfileDto>>();
.GetJsonAsync<IList<QualityProfileDto>>(cancellationToken: ct);
return response.Select(x => x.ReverseItems()).ToList();
}
public async Task<QualityProfileDto> GetSchema()
public async Task<QualityProfileDto> GetSchema(CancellationToken ct)
{
var response = await Request("schema")
.GetJsonAsync<QualityProfileDto>();
.GetJsonAsync<QualityProfileDto>(cancellationToken: ct);
return response.ReverseItems();
}
public async Task UpdateQualityProfile(QualityProfileDto profile)
public async Task UpdateQualityProfile(QualityProfileDto profile, CancellationToken ct)
{
if (profile.Id is null)
{
@ -33,13 +33,13 @@ internal class QualityProfileApiService(IServarrRequestBuilder service) : IQuali
}
await Request(profile.Id)
.PutJsonAsync(profile.ReverseItems());
.PutJsonAsync(profile.ReverseItems(), cancellationToken: ct);
}
public async Task CreateQualityProfile(QualityProfileDto profile)
public async Task CreateQualityProfile(QualityProfileDto profile, CancellationToken ct)
{
var response = await Request()
.PostJsonAsync(profile.ReverseItems())
.PostJsonAsync(profile.ReverseItems(), cancellationToken: ct)
.ReceiveJson<QualityProfileDto>();
profile.Id = response.Id;

@ -2,5 +2,5 @@ namespace Recyclarr.ServarrApi.System;
public interface ISystemApiService
{
Task<SystemStatus> GetStatus();
Task<SystemStatus> GetStatus(CancellationToken ct);
}

@ -4,9 +4,9 @@ namespace Recyclarr.ServarrApi.System;
public class SystemApiService(IServarrRequestBuilder service) : ISystemApiService
{
public async Task<SystemStatus> GetStatus()
public async Task<SystemStatus> GetStatus(CancellationToken ct)
{
return await service.Request("system", "status")
.GetJsonAsync<SystemStatus>();
.GetJsonAsync<SystemStatus>(cancellationToken: ct);
}
}

@ -26,7 +26,7 @@ public class CustomFormatServiceTest : IntegrationTestFixture
});
var sut = scope.Resolve<CustomFormatApiService>();
var result = await sut.GetCustomFormats();
var result = await sut.GetCustomFormats(CancellationToken.None);
result.Should().HaveCountGreaterThan(5);
}

@ -91,7 +91,7 @@ public abstract class IntegrationTestFixture : IDisposable
builder.RegisterMockFor<IServiceInformation>(m =>
{
// By default, choose some extremely high number so that all the newest features are enabled.
m.GetVersion().ReturnsForAnyArgs(_ => new Version("99.0.0.0"));
m.GetVersion(CancellationToken.None).ReturnsForAnyArgs(_ => new Version("99.0.0.0"));
});
}

@ -13,10 +13,10 @@ public class SonarrCapabilityEnforcerTest
{
var min = SonarrCapabilities.MinimumVersion;
fetcher.GetCapabilities().ReturnsForAnyArgs(
fetcher.GetCapabilities(CancellationToken.None).ReturnsForAnyArgs(
new SonarrCapabilities(new Version(min.Major - 1, min.Minor, min.Build, min.Revision)));
var act = sut.Check;
var act = () => sut.Check(CancellationToken.None);
act.Should().ThrowAsync<ServiceIncompatibilityException>().WithMessage("*minimum*");
}

Loading…
Cancel
Save