Files
Cleanuparr/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/GenericHandlerTests.cs
T

687 lines
25 KiB
C#

using Cleanuparr.Domain.Entities.Arr;
using Cleanuparr.Domain.Entities.Arr.Queue;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Events.Interfaces;
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Infrastructure.Features.DownloadClient;
using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
using Cleanuparr.Infrastructure.Features.Jobs;
using Cleanuparr.Infrastructure.Helpers;
using Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Events;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using MassTransit;
using Microsoft.EntityFrameworkCore;
using Cleanuparr.Infrastructure.Interceptors;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Shouldly;
using Xunit;
namespace Cleanuparr.Infrastructure.Tests.Features.Jobs;
public class GenericHandlerTests : IClassFixture<JobHandlerFixture>
{
private readonly JobHandlerFixture _fixture;
private readonly TestHandler _handler;
public GenericHandlerTests(JobHandlerFixture fixture)
{
_fixture = fixture;
_fixture.RecreateDataContext();
_fixture.ResetMocks();
_handler = new TestHandler(
Substitute.For<ILogger<GenericHandler>>(),
_fixture.DataContext,
_fixture.Cache,
_fixture.MessageBus,
_fixture.ArrClientFactory,
_fixture.ArrQueueIterator,
_fixture.DownloadServiceFactory,
_fixture.EventPublisher,
_fixture.DryRunInterceptor);
}
#region GetRecordSearchItem
[Fact]
public void GetRecordSearchItem_SonarrSingleEpisode_ReturnsEpisodeSeriesSearchItem()
{
// Arrange
var record = NewRecord(seriesId: 10, episodeId: 99);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Sonarr, 4f, record, isPack: false);
// Assert
var seriesItem = item.ShouldBeOfType<SeriesSearchItem>();
seriesItem.Id.ShouldBe(99);
seriesItem.SeriesId.ShouldBe(10);
seriesItem.SearchType.ShouldBe(SeriesSearchType.Episode);
}
[Fact]
public void GetRecordSearchItem_SonarrPack_ReturnsSeasonSeriesSearchItem()
{
// Arrange
var record = NewRecord(seriesId: 10, seasonNumber: 3);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Sonarr, 4f, record, isPack: true);
// Assert
var seriesItem = item.ShouldBeOfType<SeriesSearchItem>();
seriesItem.Id.ShouldBe(3);
seriesItem.SeriesId.ShouldBe(10);
seriesItem.SearchType.ShouldBe(SeriesSearchType.Season);
}
[Fact]
public void GetRecordSearchItem_Radarr_ReturnsMovieIdSearchItem()
{
// Arrange
var record = NewRecord(movieId: 77);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Radarr, 4f, record);
// Assert
item.ShouldBeOfType<SearchItem>();
item.Id.ShouldBe(77);
}
[Fact]
public void GetRecordSearchItem_Lidarr_ReturnsAlbumIdSearchItem()
{
// Arrange
var record = NewRecord(albumId: 55);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Lidarr, 1f, record);
// Assert
item.Id.ShouldBe(55);
}
[Fact]
public void GetRecordSearchItem_Readarr_ReturnsBookIdSearchItem()
{
// Arrange
var record = NewRecord(bookId: 42);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Readarr, 1f, record);
// Assert
item.Id.ShouldBe(42);
}
[Fact]
public void GetRecordSearchItem_WhisparrV2SingleEpisode_ReturnsSeriesSearchItem()
{
// Arrange
var record = NewRecord(seriesId: 5, episodeId: 13);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Whisparr, version: 2f, record, isPack: false);
// Assert
var seriesItem = item.ShouldBeOfType<SeriesSearchItem>();
seriesItem.Id.ShouldBe(13);
seriesItem.SeriesId.ShouldBe(5);
seriesItem.SearchType.ShouldBe(SeriesSearchType.Episode);
}
[Fact]
public void GetRecordSearchItem_WhisparrV2Pack_ReturnsSeasonSearchItem()
{
// Arrange
var record = NewRecord(seriesId: 5, seasonNumber: 2);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Whisparr, version: 2f, record, isPack: true);
// Assert
var seriesItem = item.ShouldBeOfType<SeriesSearchItem>();
seriesItem.Id.ShouldBe(2);
seriesItem.SeriesId.ShouldBe(5);
seriesItem.SearchType.ShouldBe(SeriesSearchType.Season);
}
[Fact]
public void GetRecordSearchItem_WhisparrV3_ReturnsMovieIdSearchItem()
{
// Arrange
var record = NewRecord(movieId: 88);
// Act
var item = _handler.PublicGetRecordSearchItem(InstanceType.Whisparr, version: 3f, record);
// Assert
item.ShouldBeOfType<SearchItem>();
item.Id.ShouldBe(88);
}
#endregion
#region ProcessArrConfigAsync
[Fact]
public async Task ProcessArrConfigAsync_NoEnabledInstances_SkipsProcessing()
{
// Arrange
var config = new ArrConfig
{
Type = InstanceType.Sonarr,
Instances = new List<ArrInstance>
{
new ArrInstance { Name = "n", Url = new Uri("http://x"), ApiKey = "k", Enabled = false },
},
};
// Act
await _handler.PublicProcessArrConfigAsync(config);
// Assert
_handler.ProcessInstanceCalls.ShouldBeEmpty();
}
[Fact]
public async Task ProcessArrConfigAsync_EnabledInstances_ProcessesEach()
{
// Arrange
var a = new ArrInstance { Name = "a", Url = new Uri("http://a"), ApiKey = "k", Enabled = true };
var b = new ArrInstance { Name = "b", Url = new Uri("http://b"), ApiKey = "k", Enabled = true };
var disabled = new ArrInstance { Name = "c", Url = new Uri("http://c"), ApiKey = "k", Enabled = false };
var config = new ArrConfig
{
Type = InstanceType.Sonarr,
Instances = new List<ArrInstance> { a, b, disabled },
};
// Act
await _handler.PublicProcessArrConfigAsync(config);
// Assert
_handler.ProcessInstanceCalls.Select(i => i.Name).ShouldBe(new[] { "a", "b" });
}
[Fact]
public async Task ProcessArrConfigAsync_InstanceThrowsAndThrowOnFailureTrue_Rethrows()
{
// Arrange
_handler.ProcessInstanceBehavior = _ => throw new InvalidOperationException("boom");
var config = new ArrConfig
{
Type = InstanceType.Sonarr,
Instances = new List<ArrInstance>
{
new ArrInstance { Name = "boom", Url = new Uri("http://x"), ApiKey = "k", Enabled = true },
},
};
// Act / Assert
await Should.ThrowAsync<InvalidOperationException>(
() => _handler.PublicProcessArrConfigAsync(config, throwOnFailure: true));
}
[Fact]
public async Task ProcessArrConfigAsync_InstanceThrowsAndThrowOnFailureFalse_LogsAndContinues()
{
// Arrange — first instance throws, second succeeds
_handler.ProcessInstanceBehavior = instance =>
{
if (instance.Name == "bad")
{
throw new InvalidOperationException("boom");
}
return Task.CompletedTask;
};
var config = new ArrConfig
{
Type = InstanceType.Sonarr,
Instances = new List<ArrInstance>
{
new ArrInstance { Name = "bad", Url = new Uri("http://x"), ApiKey = "k", Enabled = true },
new ArrInstance { Name = "good", Url = new Uri("http://y"), ApiKey = "k", Enabled = true },
},
};
// Act
await _handler.PublicProcessArrConfigAsync(config, throwOnFailure: false);
// Assert
_handler.ProcessInstanceCalls.Select(i => i.Name).ShouldBe(new[] { "bad", "good" });
}
#endregion
#region PublishQueueItemRemoveRequest
[Fact]
public async Task PublishQueueItemRemoveRequest_AlreadyMarked_SkipsPublish()
{
// Arrange
var arrConfig = new ArrConfig { Type = InstanceType.Sonarr, Instances = [] };
var instance = new ArrInstance
{
Name = "s",
Url = new Uri("http://s"),
ApiKey = "k",
ArrConfig = arrConfig,
Version = 4f,
};
QueueRecord record = NewRecord(seriesId: 1, episodeId: 2);
_fixture.Cache.Set(CacheKeys.DownloadMarkedForRemoval(record.DownloadId, instance.Url), true);
// Act
await _handler.PublicPublishQueueItemRemoveRequest(
instance,
record,
isPack: false,
removeFromClient: true,
DeleteReason.FailedImport);
// Assert
await _fixture.MessageBus.DidNotReceiveWithAnyArgs().Publish(default(object)!, default(CancellationToken));
}
[Fact]
public async Task PublishQueueItemRemoveRequest_Sonarr_PublishesSeriesSearchItemMessage()
{
// Arrange
var arrConfig = new ArrConfig { Type = InstanceType.Sonarr, Instances = [] };
var instance = new ArrInstance
{
Name = "s",
Url = new Uri("http://s"),
ApiKey = "k",
ArrConfig = arrConfig,
Version = 4f,
};
var record = NewRecord(seriesId: 1, episodeId: 2);
// Act
await _handler.PublicPublishQueueItemRemoveRequest(
instance, record, isPack: false, removeFromClient: true, DeleteReason.FailedImport);
// Assert
await _fixture.MessageBus.Received(1)
.Publish(Arg.Any<QueueItemRemoveRequest>(), Arg.Any<CancellationToken>());
await _fixture.EventPublisher.Received(1).PublishAsync(
EventType.DownloadMarkedForDeletion, Arg.Any<string>(), Arg.Any<EventSeverity>(),
Arg.Any<Action<AppEvent>?>(), Arg.Any<Guid?>(), Arg.Any<Guid?>(), Arg.Any<bool?>());
}
[Fact]
public async Task PublishQueueItemRemoveRequest_Radarr_PublishesGenericSearchItemMessage()
{
// Arrange
var arrConfig = new ArrConfig { Type = InstanceType.Radarr, Instances = [] };
var instance = new ArrInstance
{
Name = "r",
Url = new Uri("http://r"),
ApiKey = "k",
ArrConfig = arrConfig,
Version = 4f,
};
var record = NewRecord(movieId: 9);
// Act
await _handler.PublicPublishQueueItemRemoveRequest(
instance, record, isPack: false, removeFromClient: false, DeleteReason.Stalled);
// Assert
await _fixture.MessageBus.Received(1)
.Publish(Arg.Any<QueueItemRemoveRequest>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task PublishQueueItemRemoveRequest_WhisparrV2_PublishesSeriesSearchItemMessage()
{
// Arrange
var arrConfig = new ArrConfig { Type = InstanceType.Whisparr, Instances = [] };
var instance = new ArrInstance
{
Name = "w2",
Url = new Uri("http://w"),
ApiKey = "k",
ArrConfig = arrConfig,
Version = 2f,
};
var record = NewRecord(seriesId: 1, episodeId: 2);
// Act
await _handler.PublicPublishQueueItemRemoveRequest(
instance, record, isPack: false, removeFromClient: true, DeleteReason.FailedImport);
// Assert
await _fixture.MessageBus.Received(1)
.Publish(Arg.Any<QueueItemRemoveRequest>(), Arg.Any<CancellationToken>());
}
[Fact]
public async Task PublishQueueItemRemoveRequest_MarksDownloadBeforePublishing()
{
// Arrange
ArrConfig arrConfig = new() { Type = InstanceType.Sonarr, Instances = [] };
ArrInstance instance = new()
{
Name = "s",
Url = new Uri("http://s"),
ApiKey = "k",
ArrConfig = arrConfig,
Version = 4f,
};
QueueRecord record = NewRecord(seriesId: 1, episodeId: 2);
string key = CacheKeys.DownloadMarkedForRemoval(record.DownloadId, instance.Url);
bool markedWhenPublished = false;
_fixture.MessageBus
.When(bus => bus.Publish(Arg.Any<QueueItemRemoveRequest>(), Arg.Any<CancellationToken>()))
.Do(_ => markedWhenPublished = _fixture.Cache.TryGetValue(key, out bool _));
// Act
await _handler.PublicPublishQueueItemRemoveRequest(
instance, record, isPack: false, removeFromClient: true, DeleteReason.FailedImport);
// Assert
markedWhenPublished.ShouldBeTrue();
}
[Fact]
public async Task PublishQueueItemRemoveRequest_PublishThrows_RemovesMark()
{
// Arrange
ArrConfig arrConfig = new() { Type = InstanceType.Sonarr, Instances = [] };
ArrInstance instance = new()
{
Name = "s",
Url = new Uri("http://s"),
ApiKey = "k",
ArrConfig = arrConfig,
Version = 4f,
};
QueueRecord record = NewRecord(seriesId: 1, episodeId: 2);
string key = CacheKeys.DownloadMarkedForRemoval(record.DownloadId, instance.Url);
_fixture.MessageBus
.Publish(Arg.Any<QueueItemRemoveRequest>(), Arg.Any<CancellationToken>())
.Returns(Task.FromException(new InvalidOperationException("bus is down")));
// Act & Assert
await Should.ThrowAsync<InvalidOperationException>(() => _handler.PublicPublishQueueItemRemoveRequest(
instance, record, isPack: false, removeFromClient: true, DeleteReason.FailedImport));
_fixture.Cache.TryGetValue(key, out bool _).ShouldBeFalse();
}
#endregion
#region GetInitializedDownloadServicesAsync
[Fact]
public async Task GetInitializedDownloadServicesAsync_NoClientsInContext_ReturnsEmpty()
{
// Arrange
ContextProvider.Set(nameof(DownloadClientConfig), new List<DownloadClientConfig>());
// Act
var services = await _handler.PublicGetInitializedDownloadServicesAsync();
// Assert
services.ShouldBeEmpty();
}
[Fact]
public async Task GetInitializedDownloadServicesAsync_AllSucceed_ReturnsAll()
{
// Arrange
var clientA = new DownloadClientConfig
{
Id = Guid.NewGuid(),
Name = "a",
Type = DownloadClientType.Torrent,
TypeName = DownloadClientTypeName.qBittorrent,
};
var clientB = new DownloadClientConfig
{
Id = Guid.NewGuid(),
Name = "b",
Type = DownloadClientType.Torrent,
TypeName = DownloadClientTypeName.Deluge,
};
ContextProvider.Set(nameof(DownloadClientConfig), new List<DownloadClientConfig> { clientA, clientB });
var serviceA = Substitute.For<IDownloadService>();
serviceA.LoginAsync().Returns(Task.CompletedTask);
var serviceB = Substitute.For<IDownloadService>();
serviceB.LoginAsync().Returns(Task.CompletedTask);
_fixture.DownloadServiceFactory.GetDownloadService(clientA).Returns(serviceA);
_fixture.DownloadServiceFactory.GetDownloadService(clientB).Returns(serviceB);
// Act
var services = await _handler.PublicGetInitializedDownloadServicesAsync();
// Assert
services.Count.ShouldBe(2);
}
[Fact]
public async Task GetInitializedDownloadServicesAsync_LoginFailureForOne_SkipsThatOne()
{
// Arrange
var clientA = new DownloadClientConfig
{
Id = Guid.NewGuid(),
Name = "a",
Type = DownloadClientType.Torrent,
TypeName = DownloadClientTypeName.qBittorrent,
};
var clientB = new DownloadClientConfig
{
Id = Guid.NewGuid(),
Name = "b",
Type = DownloadClientType.Torrent,
TypeName = DownloadClientTypeName.Deluge,
};
ContextProvider.Set(nameof(DownloadClientConfig), new List<DownloadClientConfig> { clientA, clientB });
var serviceA = Substitute.For<IDownloadService>();
serviceA.LoginAsync().Returns(Task.FromException(new InvalidOperationException("login failed")));
var serviceB = Substitute.For<IDownloadService>();
serviceB.LoginAsync().Returns(Task.CompletedTask);
_fixture.DownloadServiceFactory.GetDownloadService(clientA).Returns(serviceA);
_fixture.DownloadServiceFactory.GetDownloadService(clientB).Returns(serviceB);
// Act
var services = await _handler.PublicGetInitializedDownloadServicesAsync();
// Assert
services.Count.ShouldBe(1);
services[0].ShouldBeSameAs(serviceB);
}
#endregion
#region ExecuteAsync populates context
[Fact]
public async Task ExecuteAsync_PopulatesContextWithAllConfigs()
{
// Act
await _handler.ExecuteAsync();
// Assert — every key required downstream is captured inside ExecuteInternalAsync,
// since AsyncLocal writes don't propagate back to the caller
_handler.ExecuteInternalInvoked.ShouldBeTrue();
_handler.CapturedGeneralConfig.ShouldNotBeNull();
_handler.CapturedSonarrConfig!.Type.ShouldBe(InstanceType.Sonarr);
_handler.CapturedRadarrConfig!.Type.ShouldBe(InstanceType.Radarr);
_handler.CapturedLidarrConfig!.Type.ShouldBe(InstanceType.Lidarr);
_handler.CapturedReadarrConfig!.Type.ShouldBe(InstanceType.Readarr);
_handler.CapturedWhisparrConfig!.Type.ShouldBe(InstanceType.Whisparr);
_handler.CapturedSportarrConfig!.Type.ShouldBe(InstanceType.Sportarr);
_handler.CapturedQueueCleanerConfig.ShouldNotBeNull();
_handler.CapturedContentBlockerConfig.ShouldNotBeNull();
_handler.CapturedDownloadCleanerConfig.ShouldNotBeNull();
_handler.CapturedDownloadClients.ShouldNotBeNull();
}
[Fact]
public async Task ExecuteAsync_OnlyEnabledDownloadClientsInContext()
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext, name: "enabled-1", enabled: true);
TestDataContextFactory.AddDownloadClient(_fixture.DataContext, name: "disabled-1", typeName: DownloadClientTypeName.Deluge, enabled: false);
// Act
await _handler.ExecuteAsync();
// Assert
_handler.CapturedDownloadClients.ShouldNotBeNull();
_handler.CapturedDownloadClients!.Count.ShouldBe(1);
_handler.CapturedDownloadClients[0].Name.ShouldBe("enabled-1");
}
// A rolled-back build has no service for a client kind it does not know.
// The job must never see the row.
[Theory]
[InlineData("TypeName")]
[InlineData("Type")]
public async Task ExecuteAsync_ExcludesDownloadClientsOfAnUnknownKind(string column)
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext, name: "supported", enabled: true);
DownloadClientConfig unknown = TestDataContextFactory.AddDownloadClient(
_fixture.DataContext, name: "from the future", typeName: DownloadClientTypeName.Deluge, enabled: true);
await _fixture.DataContext.Database.ExecuteSqlRawAsync(
$"UPDATE download_clients SET \"{column}\" = 'fromthefuture' WHERE \"Id\" = {{0}}",
unknown.Id);
// Act
await _handler.ExecuteAsync();
// Assert
_handler.CapturedDownloadClients.ShouldNotBeNull();
_handler.CapturedDownloadClients!.Select(c => c.Name).ShouldBe(["supported"]);
}
#endregion
private static QueueRecord NewRecord(
long id = 1,
string downloadId = "h",
long seriesId = 0,
long episodeId = 0,
long seasonNumber = 0,
long movieId = 0,
long albumId = 0,
long bookId = 0)
{
return new QueueRecord
{
Id = id,
Title = $"item-{id}",
DownloadId = downloadId,
Protocol = "torrent",
SeriesId = seriesId,
EpisodeId = episodeId,
SeasonNumber = seasonNumber,
MovieId = movieId,
AlbumId = albumId,
BookId = bookId,
};
}
/// <summary>
/// Concrete GenericHandler subclass exposing protected members for testing.
/// </summary>
private sealed class TestHandler : GenericHandler
{
public List<ArrInstance> ProcessInstanceCalls { get; } = [];
public Func<ArrInstance, Task> ProcessInstanceBehavior { get; set; } = _ => Task.CompletedTask;
public bool ExecuteInternalInvoked { get; private set; }
// Snapshot of ContextProvider values, captured inside ExecuteInternalAsync.
// (AsyncLocal writes from ExecuteAsync don't propagate back to the caller.)
public GeneralConfig? CapturedGeneralConfig { get; private set; }
public ArrConfig? CapturedSonarrConfig { get; private set; }
public ArrConfig? CapturedRadarrConfig { get; private set; }
public ArrConfig? CapturedLidarrConfig { get; private set; }
public ArrConfig? CapturedReadarrConfig { get; private set; }
public ArrConfig? CapturedWhisparrConfig { get; private set; }
public ArrConfig? CapturedSportarrConfig { get; private set; }
public QueueCleanerConfig? CapturedQueueCleanerConfig { get; private set; }
public ContentBlockerConfig? CapturedContentBlockerConfig { get; private set; }
public DownloadCleanerConfig? CapturedDownloadCleanerConfig { get; private set; }
public List<DownloadClientConfig>? CapturedDownloadClients { get; private set; }
public TestHandler(
ILogger<GenericHandler> logger,
DataContext dataContext,
IMemoryCache cache,
IBus messageBus,
IArrClientFactory arrClientFactory,
IArrQueueIterator arrQueueIterator,
IDownloadServiceFactory downloadServiceFactory,
IEventPublisher eventPublisher,
IDryRunInterceptor dryRunInterceptor)
: base(logger, dataContext, cache, messageBus, arrClientFactory, arrQueueIterator, downloadServiceFactory, eventPublisher, dryRunInterceptor)
{
}
protected override Task ExecuteInternalAsync(CancellationToken cancellationToken = default)
{
ExecuteInternalInvoked = true;
CapturedGeneralConfig = ContextProvider.Get(nameof(GeneralConfig)) as GeneralConfig;
CapturedSonarrConfig = ContextProvider.Get(nameof(InstanceType.Sonarr)) as ArrConfig;
CapturedRadarrConfig = ContextProvider.Get(nameof(InstanceType.Radarr)) as ArrConfig;
CapturedLidarrConfig = ContextProvider.Get(nameof(InstanceType.Lidarr)) as ArrConfig;
CapturedReadarrConfig = ContextProvider.Get(nameof(InstanceType.Readarr)) as ArrConfig;
CapturedWhisparrConfig = ContextProvider.Get(nameof(InstanceType.Whisparr)) as ArrConfig;
CapturedSportarrConfig = ContextProvider.Get(nameof(InstanceType.Sportarr)) as ArrConfig;
CapturedQueueCleanerConfig = ContextProvider.Get(nameof(QueueCleanerConfig)) as QueueCleanerConfig;
CapturedContentBlockerConfig = ContextProvider.Get(nameof(ContentBlockerConfig)) as ContentBlockerConfig;
CapturedDownloadCleanerConfig = ContextProvider.Get(nameof(DownloadCleanerConfig)) as DownloadCleanerConfig;
CapturedDownloadClients = ContextProvider.Get(nameof(DownloadClientConfig)) as List<DownloadClientConfig>;
return Task.CompletedTask;
}
protected override Task ProcessInstanceAsync(ArrInstance instance)
{
ProcessInstanceCalls.Add(instance);
return ProcessInstanceBehavior(instance);
}
public SearchItem PublicGetRecordSearchItem(InstanceType type, float version, QueueRecord record, bool isPack = false)
=> GetRecordSearchItem(type, version, record, isPack);
public Task PublicProcessArrConfigAsync(ArrConfig config, bool throwOnFailure = false)
=> ProcessArrConfigAsync(config, throwOnFailure);
public Task PublicPublishQueueItemRemoveRequest(
ArrInstance instance,
QueueRecord record,
bool isPack,
bool removeFromClient,
DeleteReason deleteReason,
bool skipSearch = false,
DownloadClientConfig? downloadClient = null,
bool changeCategory = false)
=> PublishQueueItemRemoveRequest(instance, record, isPack, removeFromClient, deleteReason, skipSearch, downloadClient, changeCategory);
public async Task<IReadOnlyList<IDownloadService>> PublicGetInitializedDownloadServicesAsync()
=> await GetInitializedDownloadServicesAsync();
}
}