Compare commits

..

8 Commits

Author SHA1 Message Date
Flaminel 5dced28228 fixed errors on download cleaner when download client is none (#67) 2025-02-24 12:43:06 +02:00
Flaminel 51bdaf64e4 Fix interceptor memory leaks (#66) 2025-02-23 17:50:08 +02:00
Flaminel 9c8e0ebedc updated README 2025-02-18 13:16:49 +02:00
Flaminel e1bea8a8c8 updated README 2025-02-17 23:59:36 +02:00
Marius Nechifor a6d3820104 Improve Transmission category detection (#62) 2025-02-17 02:48:27 +02:00
Flaminel 36c793a5fb updated chart values 2025-02-16 17:43:35 +02:00
Flaminel aade8a91c3 fixed dummy download service 2025-02-16 12:17:31 +02:00
Flaminel 3fe7c3de1a added null check for torrent properties 2025-02-16 03:37:50 +02:00
32 changed files with 270 additions and 207 deletions
+35 -12
View File
@@ -8,12 +8,42 @@ cleanuperr is a tool for automating the cleanup of unwanted or blocked files in
cleanuperr was created primarily to address malicious files, such as `*.lnk` or `*.zipx`, that were getting stuck in Sonarr/Radarr and required manual intervention. Some of the reddit posts that made cleanuperr come to life can be found [here](https://www.reddit.com/r/sonarr/comments/1gqnx16/psa_sonarr_downloaded_a_virus/), [here](https://www.reddit.com/r/sonarr/comments/1gqwklr/sonar_downloaded_a_mkv_file_which_looked_like_a/), [here](https://www.reddit.com/r/sonarr/comments/1gpw2wa/downloaded_waiting_to_import/) and [here](https://www.reddit.com/r/sonarr/comments/1gpi344/downloads_not_importing_no_files_found/). cleanuperr was created primarily to address malicious files, such as `*.lnk` or `*.zipx`, that were getting stuck in Sonarr/Radarr and required manual intervention. Some of the reddit posts that made cleanuperr come to life can be found [here](https://www.reddit.com/r/sonarr/comments/1gqnx16/psa_sonarr_downloaded_a_virus/), [here](https://www.reddit.com/r/sonarr/comments/1gqwklr/sonar_downloaded_a_mkv_file_which_looked_like_a/), [here](https://www.reddit.com/r/sonarr/comments/1gpw2wa/downloaded_waiting_to_import/) and [here](https://www.reddit.com/r/sonarr/comments/1gpi344/downloads_not_importing_no_files_found/).
The tool supports both qBittorrent's built-in exclusion features and its own blocklist-based system. Binaries for all platforms are provided, along with Docker images for easy deployment. > [!IMPORTANT]
> **Features:**
> - Strike system to mark stalled or downloads stuck in metadata downloading.
> - Remove and block downloads that reached a maximum number of strikes.
> - Remove downloads blocked by qBittorrent or by cleanuperr's **content blocker**.
> - Trigger a search for downloads removed from the *arrs.
> - Clean up downloads that have been seeding for a certain amount of time.
> - Notify on strike or download removal.
# cleanuperr supports both qBittorrent's built-in exclusion features and its own blocklist-based system. Binaries for all platforms are provided, along with Docker images for easy deployment.
> [!WARNING]
> This tool is actively developed and still a work in progress, so using the `latest` Docker tag may result in breaking changes. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together:
>
> https://discord.gg/sWggpnmGNY
## Table of contents:
- [Naming choice](README.md#naming-choice)
- [Quick Start](README.md#quick-start)
- [How it works](README.md#how-it-works)
- [Setup](README.md#setup)
- [Usage](README.md#usage)
- [Docker Compose](README.md#docker-compose-yaml)
- [Environment Variables](README.md#environment-variables)
- [Binaries](README.md#binaries-if-youre-not-using-docker)
- [Credits](README.md#credits)
## Naming choice
I've had people asking why it's `cleanuperr` and not `cleanuparr` and that I should change it. This name was intentional.
I've seen a few discussions on this type of naming and I've decided that I didn't deserve the `arr` moniker since `cleanuperr` is not a fork of `NZB.Drone` and it does not have any affiliation with the arrs. I still wanted to keep the naming style close enough though, to suggest a correlation between them.
## Quick Start
> [!NOTE] > [!NOTE]
> ### Quick Start
> >
> 1. **Docker (Recommended)** > 1. **Docker (Recommended)**
> Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`. > Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`.
@@ -27,10 +57,6 @@ The tool supports both qBittorrent's built-in exclusion features and its own blo
> [!TIP] > [!TIP]
> Refer to the [Environment variables](#Environment-variables) section for detailed configuration instructions and the [Setup](#Setup) section for an in-depth explanation of the cleanup process. > Refer to the [Environment variables](#Environment-variables) section for detailed configuration instructions and the [Setup](#Setup) section for an in-depth explanation of the cleanup process.
## Key features
- Marks unwanted files as skip/unwanted in the download client.
- Automatically strikes stalled or stuck downloads.
- Removes and blocks downloads that reached the maximum number of strikes or are marked as unwanted by the download client or by cleanuperr and triggers a search for removed downloads.
> [!IMPORTANT] > [!IMPORTANT]
> Only the **latest versions** of the following apps are supported, or earlier versions that have the same API as the latest version: > Only the **latest versions** of the following apps are supported, or earlier versions that have the same API as the latest version:
@@ -41,10 +67,6 @@ The tool supports both qBittorrent's built-in exclusion features and its own blo
> - Radarr > - Radarr
> - Lidarr > - Lidarr
This tool is actively developed and still a work in progress, so using the `latest` Docker tag may result in breaking changes. Join the Discord server if you want to reach out to me quickly (or just stay updated on new releases) so we can squash those pesky bugs together:
> https://discord.gg/sWggpnmGNY
# How it works # How it works
1. **Content blocker** will: 1. **Content blocker** will:
@@ -102,7 +124,8 @@ This tool is actively developed and still a work in progress, so using the `late
3. Optionally set failed import message patterns to ignore using `QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>`. 3. Optionally set failed import message patterns to ignore using `QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__<NUMBER>`.
4. Set `DOWNLOAD_CLIENT` to `none`. 4. Set `DOWNLOAD_CLIENT` to `none`.
**No other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).** > [!WARNING]
> When `DOWNLOAD_CLIENT=none`, no other action involving a download client would work (e.g. content blocking, removing stalled downloads, excluding private trackers).
## Usage ## Usage
+27 -1
View File
@@ -10,6 +10,9 @@ deployment:
repository: ghcr.io/flmorg/cleanuperr repository: ghcr.io/flmorg/cleanuperr
tag: latest tag: latest
env: env:
- name: DRY_RUN
value: "false"
- name: LOGGING__LOGLEVEL - name: LOGGING__LOGLEVEL
value: Debug value: Debug
- name: LOGGING__FILE__ENABLED - name: LOGGING__FILE__ENABLED
@@ -18,6 +21,7 @@ deployment:
value: /var/logs value: /var/logs
- name: LOGGING__ENHANCED - name: LOGGING__ENHANCED
value: "true" value: "true"
- name: TRIGGERS__QUEUECLEANER - name: TRIGGERS__QUEUECLEANER
value: 0 0/5 * * * ? value: 0 0/5 * * * ?
- name: TRIGGERS__CONTENTBLOCKER - name: TRIGGERS__CONTENTBLOCKER
@@ -47,6 +51,9 @@ deployment:
- name: CONTENTBLOCKER__DELETE_PRIVATE - name: CONTENTBLOCKER__DELETE_PRIVATE
value: "false" value: "false"
- name: DOWNLOADCLEANER__ENABLED
value: "false"
- name: DOWNLOAD_CLIENT - name: DOWNLOAD_CLIENT
value: qbittorrent value: qbittorrent
- name: QBITTORRENT__URL - name: QBITTORRENT__URL
@@ -75,6 +82,17 @@ deployment:
value: http://service.radarr-low-res.svc.cluster.local value: http://service.radarr-low-res.svc.cluster.local
- name: RADARR__INSTANCES__1__URL - name: RADARR__INSTANCES__1__URL
value: http://service.radarr-high-res.svc.cluster.local value: http://service.radarr-high-res.svc.cluster.local
- name: NOTIFIARR__ON_IMPORT_FAILED_STRIKE
value: "true"
- name: NOTIFIARR__ON_STALLED_STRIKE
value: "true"
- name: NOTIFIARR__ON_QUEUE_ITEM_DELETED
value: "true"
- name: NOTIFIARR__ON_DOWNLOAD_CLEANED
value: "true"
- name: NOTIFIARR__CHANNEL_ID
value: "1340708411259748413"
envFromSecret: envFromSecret:
- secretName: qbit-auth - secretName: qbit-auth
envs: envs:
@@ -94,6 +112,10 @@ deployment:
key: RDRL_API_KEY key: RDRL_API_KEY
- name: RADARR__INSTANCES__1__APIKEY - name: RADARR__INSTANCES__1__APIKEY
key: RDRH_API_KEY key: RDRH_API_KEY
- secretName: notifiarr-auth
envs:
- name: NOTIFIARR__API_KEY
key: API_KEY
resources: resources:
requests: requests:
cpu: 0m cpu: 0m
@@ -133,4 +155,8 @@ vaultSecrets:
path: secrets/sonarr path: secrets/sonarr
templates: templates:
SNRL_API_KEY: "{% .Secrets.low_api_key %}" SNRL_API_KEY: "{% .Secrets.low_api_key %}"
SNRH_API_KEY: "{% .Secrets.high_api_key %}" SNRH_API_KEY: "{% .Secrets.high_api_key %}"
- name: notifiarr-auth
path: secrets/notifiarr
templates:
API_KEY: "{% .Secrets.passthrough_api_key %}"
+1 -31
View File
@@ -1,8 +1,6 @@
using System.Net; using System.Net;
using Castle.DynamicProxy;
using Common.Configuration.General; using Common.Configuration.General;
using Common.Helpers; using Common.Helpers;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.DownloadClient.Deluge; using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.Notifications.Consumers; using Infrastructure.Verticals.Notifications.Consumers;
using Infrastructure.Verticals.Notifications.Models; using Infrastructure.Verticals.Notifications.Models;
@@ -42,8 +40,7 @@ public static class MainDI
e.PrefetchCount = 1; e.PrefetchCount = 1;
}); });
}); });
}) });
.AddDryRunInterceptor();
private static IServiceCollection AddHttpClients(this IServiceCollection services, IConfiguration configuration) private static IServiceCollection AddHttpClients(this IServiceCollection services, IConfiguration configuration)
{ {
@@ -91,31 +88,4 @@ public static class MainDI
.OrResult(response => !response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.Unauthorized) .OrResult(response => !response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.Unauthorized)
.WaitAndRetryAsync(config.MaxRetries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))) .WaitAndRetryAsync(config.MaxRetries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)))
); );
private static IServiceCollection AddDryRunInterceptor(this IServiceCollection services)
{
services
.Where(s => s.ServiceType != typeof(IDryRunService) && typeof(IDryRunService).IsAssignableFrom(s.ServiceType))
.ToList()
.ForEach(service =>
{
services.Decorate(service.ServiceType, (target, svc) =>
{
ProxyGenerator proxyGenerator = new();
DryRunAsyncInterceptor interceptor = svc.GetRequiredService<DryRunAsyncInterceptor>();
object implementation = proxyGenerator.CreateClassProxyWithTarget(
service.ServiceType,
target,
interceptor
);
((IInterceptedService)target).Proxy = implementation;
return implementation;
});
});
return services;
}
} }
@@ -10,7 +10,7 @@ public static class NotificationsDI
.Configure<NotifiarrConfig>(configuration.GetSection(NotifiarrConfig.SectionName)) .Configure<NotifiarrConfig>(configuration.GetSection(NotifiarrConfig.SectionName))
.AddTransient<INotifiarrProxy, NotifiarrProxy>() .AddTransient<INotifiarrProxy, NotifiarrProxy>()
.AddTransient<INotificationProvider, NotifiarrProvider>() .AddTransient<INotificationProvider, NotifiarrProvider>()
.AddTransient<NotificationPublisher>() .AddTransient<INotificationPublisher, NotificationPublisher>()
.AddTransient<INotificationFactory, NotificationFactory>() .AddTransient<INotificationFactory, NotificationFactory>()
.AddTransient<NotificationService>(); .AddTransient<NotificationService>();
} }
@@ -15,7 +15,7 @@ public static class ServicesDI
{ {
public static IServiceCollection AddServices(this IServiceCollection services) => public static IServiceCollection AddServices(this IServiceCollection services) =>
services services
.AddTransient<DryRunAsyncInterceptor>() .AddTransient<IDryRunInterceptor, DryRunInterceptor>()
.AddTransient<SonarrClient>() .AddTransient<SonarrClient>()
.AddTransient<RadarrClient>() .AddTransient<RadarrClient>()
.AddTransient<LidarrClient>() .AddTransient<LidarrClient>()
@@ -1,6 +1,7 @@
using Common.Configuration.ContentBlocker; using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadCleaner; using Common.Configuration.DownloadCleaner;
using Common.Configuration.QueueCleaner; using Common.Configuration.QueueCleaner;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.DownloadClient; using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
@@ -53,7 +54,8 @@ public class DownloadServiceFixture : IDisposable
downloadCleanerOptions.Value.Returns(new DownloadCleanerConfig()); downloadCleanerOptions.Value.Returns(new DownloadCleanerConfig());
var filenameEvaluator = Substitute.For<IFilenameEvaluator>(); var filenameEvaluator = Substitute.For<IFilenameEvaluator>();
var notifier = Substitute.For<NotificationPublisher>(); var notifier = Substitute.For<INotificationPublisher>();
var dryRunInterceptor = Substitute.For<IDryRunInterceptor>();
return new TestDownloadService( return new TestDownloadService(
Logger, Logger,
@@ -63,7 +65,8 @@ public class DownloadServiceFixture : IDisposable
Cache, Cache,
filenameEvaluator, filenameEvaluator,
Striker, Striker,
notifier notifier,
dryRunInterceptor
); );
} }
@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using Common.Configuration.ContentBlocker; using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadCleaner; using Common.Configuration.DownloadCleaner;
using Common.Configuration.QueueCleaner; using Common.Configuration.QueueCleaner;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.DownloadClient; using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
@@ -23,9 +24,12 @@ public class TestDownloadService : DownloadService
IMemoryCache cache, IMemoryCache cache,
IFilenameEvaluator filenameEvaluator, IFilenameEvaluator filenameEvaluator,
IStriker striker, IStriker striker,
NotificationPublisher notifier) INotificationPublisher notifier,
: base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, IDryRunInterceptor dryRunInterceptor
cache, filenameEvaluator, striker, notifier) ) : base(
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
filenameEvaluator, striker, notifier, dryRunInterceptor
)
{ {
} }
@@ -12,7 +12,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
<PackageReference Include="FLM.QBittorrent" Version="1.0.0" /> <PackageReference Include="FLM.QBittorrent" Version="1.0.0" />
<PackageReference Include="FLM.Transmission" Version="1.0.2" /> <PackageReference Include="FLM.Transmission" Version="1.0.2" />
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
@@ -1,5 +1,4 @@
using System.Reflection; using System.Reflection;
using Castle.DynamicProxy;
using Common.Attributes; using Common.Attributes;
using Common.Configuration.General; using Common.Configuration.General;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -7,41 +6,70 @@ using Microsoft.Extensions.Options;
namespace Infrastructure.Interceptors; namespace Infrastructure.Interceptors;
public class DryRunAsyncInterceptor : AsyncInterceptorBase public class DryRunInterceptor : IDryRunInterceptor
{ {
private readonly ILogger<DryRunAsyncInterceptor> _logger; private readonly ILogger<DryRunInterceptor> _logger;
private readonly DryRunConfig _config; private readonly DryRunConfig _config;
public DryRunAsyncInterceptor(ILogger<DryRunAsyncInterceptor> logger, IOptions<DryRunConfig> config) public DryRunInterceptor(ILogger<DryRunInterceptor> logger, IOptions<DryRunConfig> config)
{ {
_logger = logger; _logger = logger;
_config = config.Value; _config = config.Value;
} }
protected override async Task InterceptAsync(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task> proceed) public void Intercept(Action action)
{ {
MethodInfo? method = invocation.MethodInvocationTarget ?? invocation.Method; MethodInfo methodInfo = action.Method;
if (IsDryRun(method))
if (IsDryRun(methodInfo))
{ {
_logger.LogInformation("[DRY RUN] skipping method: {name}", method.Name); _logger.LogInformation("[DRY RUN] skipping method: {name}", methodInfo.Name);
return; return;
} }
await proceed(invocation, proceedInfo); action();
} }
protected override async Task<TResult> InterceptAsync<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task<TResult>> proceed) public Task InterceptAsync(Delegate action, params object[] parameters)
{ {
MethodInfo? method = invocation.MethodInvocationTarget ?? invocation.Method; MethodInfo methodInfo = action.Method;
if (IsDryRun(method))
if (IsDryRun(methodInfo))
{ {
_logger.LogInformation("[DRY RUN] skipping method: {name}", method.Name); _logger.LogInformation("[DRY RUN] skipping method: {name}", methodInfo.Name);
return default!; return Task.CompletedTask;
} }
return await proceed(invocation, proceedInfo); object? result = action.DynamicInvoke(parameters);
}
if (result is Task task)
{
return task;
}
return Task.CompletedTask;
}
public Task<T?> InterceptAsync<T>(Delegate action, params object[] parameters)
{
MethodInfo methodInfo = action.Method;
if (IsDryRun(methodInfo))
{
_logger.LogInformation("[DRY RUN] skipping method: {name}", methodInfo.Name);
return Task.FromResult(default(T));
}
object? result = action.DynamicInvoke(parameters);
if (result is Task<T?> task)
{
return task;
}
return Task.FromResult(default(T));
}
private bool IsDryRun(MethodInfo method) private bool IsDryRun(MethodInfo method)
{ {
return method.GetCustomAttributes(typeof(DryRunSafeguardAttribute), true).Any() && _config.IsDryRun; return method.GetCustomAttributes(typeof(DryRunSafeguardAttribute), true).Any() && _config.IsDryRun;
@@ -0,0 +1,10 @@
namespace Infrastructure.Interceptors;
public interface IDryRunInterceptor
{
void Intercept(Action action);
Task InterceptAsync(Delegate action, params object[] parameters);
Task<T?> InterceptAsync<T>(Delegate action, params object[] parameters);
}
@@ -1,5 +0,0 @@
namespace Infrastructure.Interceptors;
public interface IDryRunService : IInterceptedService
{
}
@@ -1,6 +0,0 @@
namespace Infrastructure.Interceptors;
public interface IInterceptedService
{
public object Proxy { get; set; }
}
@@ -1,21 +0,0 @@
namespace Infrastructure.Interceptors;
public class InterceptedService : IInterceptedService
{
private object? _proxy;
public object Proxy
{
get
{
if (_proxy is null)
{
throw new Exception("Proxy is not set");
}
return _proxy;
}
set => _proxy = value;
}
}
+7 -10
View File
@@ -15,27 +15,22 @@ using Newtonsoft.Json;
namespace Infrastructure.Verticals.Arr; namespace Infrastructure.Verticals.Arr;
public abstract class ArrClient : InterceptedService, IArrClient, IDryRunService public abstract class ArrClient : IArrClient
{ {
protected readonly ILogger<ArrClient> _logger; protected readonly ILogger<ArrClient> _logger;
protected readonly HttpClient _httpClient; protected readonly HttpClient _httpClient;
protected readonly LoggingConfig _loggingConfig; protected readonly LoggingConfig _loggingConfig;
protected readonly QueueCleanerConfig _queueCleanerConfig; protected readonly QueueCleanerConfig _queueCleanerConfig;
protected readonly IStriker _striker; protected readonly IStriker _striker;
protected readonly IDryRunInterceptor _dryRunInterceptor;
/// <summary>
/// Constructor to be used by interceptors.
/// </summary>
protected ArrClient()
{
}
protected ArrClient( protected ArrClient(
ILogger<ArrClient> logger, ILogger<ArrClient> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig, IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<QueueCleanerConfig> queueCleanerConfig,
IStriker striker IStriker striker,
IDryRunInterceptor dryRunInterceptor
) )
{ {
_logger = logger; _logger = logger;
@@ -43,6 +38,7 @@ public abstract class ArrClient : InterceptedService, IArrClient, IDryRunService
_loggingConfig = loggingConfig.Value; _loggingConfig = loggingConfig.Value;
_queueCleanerConfig = queueCleanerConfig.Value; _queueCleanerConfig = queueCleanerConfig.Value;
_striker = striker; _striker = striker;
_dryRunInterceptor = dryRunInterceptor;
} }
public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page) public virtual async Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page)
@@ -125,7 +121,8 @@ public abstract class ArrClient : InterceptedService, IArrClient, IDryRunService
using HttpRequestMessage request = new(HttpMethod.Delete, uri); using HttpRequestMessage request = new(HttpMethod.Delete, uri);
SetApiKey(request, arrInstance.ApiKey); SetApiKey(request, arrInstance.ApiKey);
using var _ = await ((ArrClient)Proxy).SendRequestAsync(request); HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
response?.Dispose();
_logger.LogInformation( _logger.LogInformation(
removeFromClient removeFromClient
@@ -5,6 +5,7 @@ using Common.Configuration.QueueCleaner;
using Domain.Models.Arr; using Domain.Models.Arr;
using Domain.Models.Arr.Queue; using Domain.Models.Arr.Queue;
using Domain.Models.Lidarr; using Domain.Models.Lidarr;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.Arr.Interfaces; using Infrastructure.Verticals.Arr.Interfaces;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -15,18 +16,14 @@ namespace Infrastructure.Verticals.Arr;
public class LidarrClient : ArrClient, ILidarrClient public class LidarrClient : ArrClient, ILidarrClient
{ {
/// <inheritdoc/>
public LidarrClient()
{
}
public LidarrClient( public LidarrClient(
ILogger<LidarrClient> logger, ILogger<LidarrClient> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig, IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<QueueCleanerConfig> queueCleanerConfig,
IStriker striker IStriker striker,
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker) IDryRunInterceptor dryRunInterceptor
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker, dryRunInterceptor)
{ {
} }
@@ -64,7 +61,8 @@ public class LidarrClient : ArrClient, ILidarrClient
try try
{ {
using var _ = await ((LidarrClient)Proxy).SendRequestAsync(request); HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
response?.Dispose();
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext)); _logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
} }
@@ -5,6 +5,7 @@ using Common.Configuration.QueueCleaner;
using Domain.Models.Arr; using Domain.Models.Arr;
using Domain.Models.Arr.Queue; using Domain.Models.Arr.Queue;
using Domain.Models.Radarr; using Domain.Models.Radarr;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.Arr.Interfaces; using Infrastructure.Verticals.Arr.Interfaces;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -15,18 +16,14 @@ namespace Infrastructure.Verticals.Arr;
public class RadarrClient : ArrClient, IRadarrClient public class RadarrClient : ArrClient, IRadarrClient
{ {
/// <inheritdoc/>
public RadarrClient()
{
}
public RadarrClient( public RadarrClient(
ILogger<ArrClient> logger, ILogger<ArrClient> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig, IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<QueueCleanerConfig> queueCleanerConfig,
IStriker striker IStriker striker,
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker) IDryRunInterceptor dryRunInterceptor
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker, dryRunInterceptor)
{ {
} }
@@ -72,7 +69,8 @@ public class RadarrClient : ArrClient, IRadarrClient
try try
{ {
using var _ = await ((RadarrClient)Proxy).SendRequestAsync(request); HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
response?.Dispose();
_logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext)); _logger.LogInformation("{log}", GetSearchLog(arrInstance.Url, command, true, logContext));
} }
@@ -5,6 +5,7 @@ using Common.Configuration.QueueCleaner;
using Domain.Models.Arr; using Domain.Models.Arr;
using Domain.Models.Arr.Queue; using Domain.Models.Arr.Queue;
using Domain.Models.Sonarr; using Domain.Models.Sonarr;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.Arr.Interfaces; using Infrastructure.Verticals.Arr.Interfaces;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -16,18 +17,14 @@ namespace Infrastructure.Verticals.Arr;
public class SonarrClient : ArrClient, ISonarrClient public class SonarrClient : ArrClient, ISonarrClient
{ {
/// <inheritdoc/>
public SonarrClient()
{
}
public SonarrClient( public SonarrClient(
ILogger<SonarrClient> logger, ILogger<SonarrClient> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IOptions<LoggingConfig> loggingConfig, IOptions<LoggingConfig> loggingConfig,
IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<QueueCleanerConfig> queueCleanerConfig,
IStriker striker IStriker striker,
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker) IDryRunInterceptor dryRunInterceptor
) : base(logger, httpClientFactory, loggingConfig, queueCleanerConfig, striker, dryRunInterceptor)
{ {
} }
@@ -68,8 +65,9 @@ public class SonarrClient : ArrClient, ISonarrClient
try try
{ {
using var _ = await ((SonarrClient)Proxy).SendRequestAsync(request); HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
response?.Dispose();
_logger.LogInformation("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, true, logContext)); _logger.LogInformation("{log}", GetSearchLog(command.SearchType, arrInstance.Url, command, true, logContext));
} }
catch catch
@@ -36,7 +36,7 @@ public sealed class ContentBlocker : GenericHandler
ArrQueueIterator arrArrQueueIterator, ArrQueueIterator arrArrQueueIterator,
BlocklistProvider blocklistProvider, BlocklistProvider blocklistProvider,
DownloadServiceFactory downloadServiceFactory, DownloadServiceFactory downloadServiceFactory,
NotificationPublisher notifier INotificationPublisher notifier
) : base( ) : base(
logger, downloadClientConfig, logger, downloadClientConfig,
sonarrConfig, radarrConfig, lidarrConfig, sonarrConfig, radarrConfig, lidarrConfig,
@@ -31,7 +31,7 @@ public sealed class DownloadCleaner : GenericHandler
LidarrClient lidarrClient, LidarrClient lidarrClient,
ArrQueueIterator arrArrQueueIterator, ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory, DownloadServiceFactory downloadServiceFactory,
NotificationPublisher notifier INotificationPublisher notifier
) : base( ) : base(
logger, downloadClientConfig, logger, downloadClientConfig,
sonarrConfig, radarrConfig, lidarrConfig, sonarrConfig, radarrConfig, lidarrConfig,
@@ -46,6 +46,12 @@ public sealed class DownloadCleaner : GenericHandler
public override async Task ExecuteAsync() public override async Task ExecuteAsync()
{ {
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
{
_logger.LogWarning("download client is set to none");
return;
}
if (_config.Categories?.Count is null or 0) if (_config.Categories?.Count is null or 0)
{ {
_logger.LogWarning("no categories configured"); _logger.LogWarning("no categories configured");
@@ -7,11 +7,11 @@ using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner; using Common.Configuration.QueueCleaner;
using Domain.Enums; using Domain.Enums;
using Domain.Models.Deluge.Response; using Domain.Models.Deluge.Response;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.Context; using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
using Infrastructure.Verticals.Notifications; using Infrastructure.Verticals.Notifications;
using MassTransit.Configuration;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -22,11 +22,6 @@ public class DelugeService : DownloadService, IDelugeService
{ {
private readonly DelugeClient _client; private readonly DelugeClient _client;
/// <inheritdoc/>
public DelugeService()
{
}
public DelugeService( public DelugeService(
ILogger<DelugeService> logger, ILogger<DelugeService> logger,
IOptions<DelugeConfig> config, IOptions<DelugeConfig> config,
@@ -37,8 +32,12 @@ public class DelugeService : DownloadService, IDelugeService
IMemoryCache cache, IMemoryCache cache,
IFilenameEvaluator filenameEvaluator, IFilenameEvaluator filenameEvaluator,
IStriker striker, IStriker striker,
NotificationPublisher notifier INotificationPublisher notifier,
) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier) IDryRunInterceptor dryRunInterceptor
) : base(
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
filenameEvaluator, striker, notifier, dryRunInterceptor
)
{ {
config.Value.Validate(); config.Value.Validate();
_client = new (config, httpClientFactory); _client = new (config, httpClientFactory);
@@ -190,7 +189,7 @@ public class DelugeService : DownloadService, IDelugeService
return result; return result;
} }
await ((DelugeService)Proxy).ChangeFilesPriority(hash, sortedPriorities); await _dryRunInterceptor.InterceptAsync(ChangeFilesPriority, hash, sortedPriorities);
return result; return result;
} }
@@ -245,8 +244,8 @@ public class DelugeService : DownloadService, IDelugeService
{ {
continue; continue;
} }
await ((DelugeService)Proxy).DeleteDownload(download.Hash); await _dryRunInterceptor.InterceptAsync(DeleteDownload, download.Hash);
_logger.LogInformation( _logger.LogInformation(
"download cleaned | {reason} reached | {name}", "download cleaned | {reason} reached | {name}",
@@ -18,7 +18,7 @@ using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.DownloadClient; namespace Infrastructure.Verticals.DownloadClient;
public abstract class DownloadService : InterceptedService, IDownloadService public abstract class DownloadService : IDownloadService
{ {
protected readonly ILogger<DownloadService> _logger; protected readonly ILogger<DownloadService> _logger;
protected readonly QueueCleanerConfig _queueCleanerConfig; protected readonly QueueCleanerConfig _queueCleanerConfig;
@@ -28,15 +28,9 @@ public abstract class DownloadService : InterceptedService, IDownloadService
protected readonly IFilenameEvaluator _filenameEvaluator; protected readonly IFilenameEvaluator _filenameEvaluator;
protected readonly IStriker _striker; protected readonly IStriker _striker;
protected readonly MemoryCacheEntryOptions _cacheOptions; protected readonly MemoryCacheEntryOptions _cacheOptions;
protected readonly NotificationPublisher _notifier; protected readonly INotificationPublisher _notifier;
protected readonly IDryRunInterceptor _dryRunInterceptor;
/// <summary>
/// Constructor to be used by interceptors.
/// </summary>
protected DownloadService()
{
}
protected DownloadService( protected DownloadService(
ILogger<DownloadService> logger, ILogger<DownloadService> logger,
IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<QueueCleanerConfig> queueCleanerConfig,
@@ -45,7 +39,9 @@ public abstract class DownloadService : InterceptedService, IDownloadService
IMemoryCache cache, IMemoryCache cache,
IFilenameEvaluator filenameEvaluator, IFilenameEvaluator filenameEvaluator,
IStriker striker, IStriker striker,
NotificationPublisher notifier) INotificationPublisher notifier,
IDryRunInterceptor dryRunInterceptor
)
{ {
_logger = logger; _logger = logger;
_queueCleanerConfig = queueCleanerConfig.Value; _queueCleanerConfig = queueCleanerConfig.Value;
@@ -55,6 +51,7 @@ public abstract class DownloadService : InterceptedService, IDownloadService
_filenameEvaluator = filenameEvaluator; _filenameEvaluator = filenameEvaluator;
_striker = striker; _striker = striker;
_notifier = notifier; _notifier = notifier;
_dryRunInterceptor = dryRunInterceptor;
_cacheOptions = new MemoryCacheEntryOptions() _cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer); .SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
} }
@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using Common.Configuration.ContentBlocker; using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadCleaner; using Common.Configuration.DownloadCleaner;
using Common.Configuration.QueueCleaner; using Common.Configuration.QueueCleaner;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
using Infrastructure.Verticals.Notifications; using Infrastructure.Verticals.Notifications;
@@ -12,9 +13,9 @@ using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.DownloadClient; namespace Infrastructure.Verticals.DownloadClient;
public sealed class DummyDownloadService : DownloadService public class DummyDownloadService : DownloadService
{ {
public DummyDownloadService(ILogger<DownloadService> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<ContentBlockerConfig> contentBlockerConfig, IOptions<DownloadCleanerConfig> downloadCleanerConfig, IMemoryCache cache, IFilenameEvaluator filenameEvaluator, IStriker striker, NotificationPublisher notifier) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier) public DummyDownloadService(ILogger<DownloadService> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, IOptions<ContentBlockerConfig> contentBlockerConfig, IOptions<DownloadCleanerConfig> downloadCleanerConfig, IMemoryCache cache, IFilenameEvaluator filenameEvaluator, IStriker striker, INotificationPublisher notifier, IDryRunInterceptor dryRunInterceptor) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier, dryRunInterceptor)
{ {
} }
@@ -6,7 +6,7 @@ using Infrastructure.Interceptors;
namespace Infrastructure.Verticals.DownloadClient; namespace Infrastructure.Verticals.DownloadClient;
public interface IDownloadService : IDisposable, IDryRunService public interface IDownloadService : IDisposable
{ {
public Task LoginAsync(); public Task LoginAsync();
@@ -1,5 +1,5 @@
namespace Infrastructure.Verticals.DownloadClient.QBittorrent; namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
public interface IQBitService : IDownloadService public interface IQBitService : IDownloadService, IDisposable
{ {
} }
@@ -7,6 +7,7 @@ using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner; using Common.Configuration.QueueCleaner;
using Common.Helpers; using Common.Helpers;
using Domain.Enums; using Domain.Enums;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.Context; using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
@@ -24,11 +25,6 @@ public class QBitService : DownloadService, IQBitService
private readonly QBitConfig _config; private readonly QBitConfig _config;
private readonly QBittorrentClient _client; private readonly QBittorrentClient _client;
/// <inheritdoc/>
public QBitService()
{
}
public QBitService( public QBitService(
ILogger<QBitService> logger, ILogger<QBitService> logger,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
@@ -39,8 +35,12 @@ public class QBitService : DownloadService, IQBitService
IMemoryCache cache, IMemoryCache cache,
IFilenameEvaluator filenameEvaluator, IFilenameEvaluator filenameEvaluator,
IStriker striker, IStriker striker,
NotificationPublisher notifier INotificationPublisher notifier,
) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier) IDryRunInterceptor dryRunInterceptor
) : base(
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
filenameEvaluator, striker, notifier, dryRunInterceptor
)
{ {
_config = config.Value; _config = config.Value;
_config.Validate(); _config.Validate();
@@ -200,7 +200,7 @@ public class QBitService : DownloadService, IQBitService
foreach (int fileIndex in unwantedFiles) foreach (int fileIndex in unwantedFiles)
{ {
await ((QBitService)Proxy).SkipFile(hash, fileIndex); await _dryRunInterceptor.InterceptAsync(SkipFile, hash, fileIndex);
} }
return result; return result;
@@ -245,6 +245,12 @@ public class QBitService : DownloadService, IQBitService
{ {
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(download.Hash); TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(download.Hash);
if (torrentProperties is null)
{
_logger.LogDebug("failed to find torrent properties in the download client | {name}", download.Name);
return;
}
bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) && bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
bool.TryParse(dictValue?.ToString(), out bool boolValue) bool.TryParse(dictValue?.ToString(), out bool boolValue)
&& boolValue; && boolValue;
@@ -266,7 +272,7 @@ public class QBitService : DownloadService, IQBitService
continue; continue;
} }
await ((QBitService)Proxy).DeleteDownload(download.Hash); await _dryRunInterceptor.InterceptAsync(DeleteDownload, download.Hash);
_logger.LogInformation( _logger.LogInformation(
"download cleaned | {reason} reached | {name}", "download cleaned | {reason} reached | {name}",
@@ -7,6 +7,7 @@ using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner; using Common.Configuration.QueueCleaner;
using Common.Helpers; using Common.Helpers;
using Domain.Enums; using Domain.Enums;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.Context; using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.ItemStriker; using Infrastructure.Verticals.ItemStriker;
@@ -26,11 +27,6 @@ public class TransmissionService : DownloadService, ITransmissionService
private readonly Client _client; private readonly Client _client;
private TorrentInfo[]? _torrentsCache; private TorrentInfo[]? _torrentsCache;
/// <inheritdoc/>
public TransmissionService()
{
}
public TransmissionService( public TransmissionService(
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
ILogger<TransmissionService> logger, ILogger<TransmissionService> logger,
@@ -41,8 +37,12 @@ public class TransmissionService : DownloadService, ITransmissionService
IMemoryCache cache, IMemoryCache cache,
IFilenameEvaluator filenameEvaluator, IFilenameEvaluator filenameEvaluator,
IStriker striker, IStriker striker,
NotificationPublisher notifier INotificationPublisher notifier,
) : base(logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache, filenameEvaluator, striker, notifier) IDryRunInterceptor dryRunInterceptor
) : base(
logger, queueCleanerConfig, contentBlockerConfig, downloadCleanerConfig, cache,
filenameEvaluator, striker, notifier, dryRunInterceptor
)
{ {
_config = config.Value; _config = config.Value;
_config.Validate(); _config.Validate();
@@ -174,8 +174,8 @@ public class TransmissionService : DownloadService, ITransmissionService
} }
_logger.LogDebug("changing priorities | torrent {hash}", hash); _logger.LogDebug("changing priorities | torrent {hash}", hash);
await ((TransmissionService)Proxy).SetUnwantedFiles(torrent.Id, unwantedFiles.ToArray()); await _dryRunInterceptor.InterceptAsync(SetUnwantedFiles, torrent.Id, unwantedFiles.ToArray());
return result; return result;
} }
@@ -203,7 +203,16 @@ public class TransmissionService : DownloadService, ITransmissionService
?.Where(x => !string.IsNullOrEmpty(x.HashString)) ?.Where(x => !string.IsNullOrEmpty(x.HashString))
.Where(x => x.Status is 5 or 6) .Where(x => x.Status is 5 or 6)
.Where(x => categories .Where(x => categories
.Any(cat => x.DownloadDir?.EndsWith(cat.Name, StringComparison.InvariantCultureIgnoreCase) is true) .Any(cat =>
{
if (x.DownloadDir is null)
{
return false;
}
return Path.GetFileName(Path.TrimEndingDirectorySeparator(x.DownloadDir))
.Equals(cat.Name, StringComparison.InvariantCultureIgnoreCase);
})
) )
.Cast<object>() .Cast<object>()
.ToList(); .ToList();
@@ -220,8 +229,17 @@ public class TransmissionService : DownloadService, ITransmissionService
} }
Category? category = categoriesToClean Category? category = categoriesToClean
.FirstOrDefault(x => download.DownloadDir?.EndsWith(x.Name, StringComparison.InvariantCultureIgnoreCase) is true); .FirstOrDefault(x =>
{
if (download.DownloadDir is null)
{
return false;
}
return Path.GetFileName(Path.TrimEndingDirectorySeparator(download.DownloadDir))
.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase);
});
if (category is null) if (category is null)
{ {
continue; continue;
@@ -250,7 +268,7 @@ public class TransmissionService : DownloadService, ITransmissionService
continue; continue;
} }
await ((TransmissionService)Proxy).RemoveDownloadAsync(download.Id); await _dryRunInterceptor.InterceptAsync(RemoveDownloadAsync, download.Id);
_logger.LogInformation( _logger.LogInformation(
"download cleaned | {reason} reached | {name}", "download cleaned | {reason} reached | {name}",
@@ -1,6 +1,7 @@
using Common.Helpers; using Common.Helpers;
using Domain.Enums; using Domain.Enums;
using Infrastructure.Helpers; using Infrastructure.Helpers;
using Infrastructure.Interceptors;
using Infrastructure.Verticals.Context; using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.Notifications; using Infrastructure.Verticals.Notifications;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@@ -13,13 +14,15 @@ public sealed class Striker : IStriker
private readonly ILogger<Striker> _logger; private readonly ILogger<Striker> _logger;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
private readonly MemoryCacheEntryOptions _cacheOptions; private readonly MemoryCacheEntryOptions _cacheOptions;
private readonly NotificationPublisher _notifier; private readonly INotificationPublisher _notifier;
private readonly IDryRunInterceptor _dryRunInterceptor;
public Striker(ILogger<Striker> logger, IMemoryCache cache, NotificationPublisher notifier) public Striker(ILogger<Striker> logger, IMemoryCache cache, INotificationPublisher notifier, IDryRunInterceptor dryRunInterceptor)
{ {
_logger = logger; _logger = logger;
_cache = cache; _cache = cache;
_notifier = notifier; _notifier = notifier;
_dryRunInterceptor = dryRunInterceptor;
_cacheOptions = new MemoryCacheEntryOptions() _cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer); .SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
} }
@@ -24,7 +24,7 @@ public abstract class GenericHandler : IHandler, IDisposable
protected readonly ILidarrClient _lidarrClient; protected readonly ILidarrClient _lidarrClient;
protected readonly ArrQueueIterator _arrArrQueueIterator; protected readonly ArrQueueIterator _arrArrQueueIterator;
protected readonly IDownloadService _downloadService; protected readonly IDownloadService _downloadService;
protected readonly NotificationPublisher _notifier; protected readonly INotificationPublisher _notifier;
protected GenericHandler( protected GenericHandler(
ILogger<GenericHandler> logger, ILogger<GenericHandler> logger,
@@ -37,7 +37,7 @@ public abstract class GenericHandler : IHandler, IDisposable
ILidarrClient lidarrClient, ILidarrClient lidarrClient,
ArrQueueIterator arrArrQueueIterator, ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory, DownloadServiceFactory downloadServiceFactory,
NotificationPublisher notifier INotificationPublisher notifier
) )
{ {
_logger = logger; _logger = logger;
@@ -0,0 +1,12 @@
using Domain.Enums;
namespace Infrastructure.Verticals.Notifications;
public interface INotificationPublisher
{
Task NotifyStrike(StrikeType strikeType, int strikeCount);
Task NotifyQueueItemDeleted(bool removeFromClient, DeleteReason reason);
Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason);
}
@@ -12,25 +12,19 @@ using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.Notifications; namespace Infrastructure.Verticals.Notifications;
public class NotificationPublisher : InterceptedService, IDryRunService public class NotificationPublisher : INotificationPublisher
{ {
private readonly ILogger<NotificationPublisher> _logger; private readonly ILogger<INotificationPublisher> _logger;
private readonly IBus _messageBus; private readonly IBus _messageBus;
private readonly IDryRunInterceptor _dryRunInterceptor;
/// <summary>
/// Constructor to be used by interceptors. public NotificationPublisher(ILogger<INotificationPublisher> logger, IBus messageBus, IDryRunInterceptor dryRunInterceptor)
/// </summary>
public NotificationPublisher()
{
}
public NotificationPublisher(ILogger<NotificationPublisher> logger, IBus messageBus)
{ {
_logger = logger; _logger = logger;
_messageBus = messageBus; _messageBus = messageBus;
_dryRunInterceptor = dryRunInterceptor;
} }
[DryRunSafeguard]
public virtual async Task NotifyStrike(StrikeType strikeType, int strikeCount) public virtual async Task NotifyStrike(StrikeType strikeType, int strikeCount)
{ {
try try
@@ -54,10 +48,10 @@ public class NotificationPublisher : InterceptedService, IDryRunService
switch (strikeType) switch (strikeType)
{ {
case StrikeType.Stalled: case StrikeType.Stalled:
await _messageBus.Publish(notification.Adapt<StalledStrikeNotification>()); await _dryRunInterceptor.InterceptAsync(Notify<StalledStrikeNotification>, notification.Adapt<StalledStrikeNotification>());
break; break;
case StrikeType.ImportFailed: case StrikeType.ImportFailed:
await _messageBus.Publish(notification.Adapt<FailedImportStrikeNotification>()); await _dryRunInterceptor.InterceptAsync(Notify<FailedImportStrikeNotification>, notification.Adapt<FailedImportStrikeNotification>());
break; break;
} }
} }
@@ -67,7 +61,6 @@ public class NotificationPublisher : InterceptedService, IDryRunService
} }
} }
[DryRunSafeguard]
public virtual async Task NotifyQueueItemDeleted(bool removeFromClient, DeleteReason reason) public virtual async Task NotifyQueueItemDeleted(bool removeFromClient, DeleteReason reason)
{ {
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord)); QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
@@ -86,10 +79,9 @@ public class NotificationPublisher : InterceptedService, IDryRunService
Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }] Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }]
}; };
await _messageBus.Publish(notification); await _dryRunInterceptor.InterceptAsync(Notify<QueueItemDeletedNotification>, notification);
} }
[DryRunSafeguard]
public virtual async Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason) public virtual async Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason)
{ {
DownloadCleanedNotification notification = new() DownloadCleanedNotification notification = new()
@@ -106,7 +98,13 @@ public class NotificationPublisher : InterceptedService, IDryRunService
Level = NotificationLevel.Important Level = NotificationLevel.Important
}; };
await _messageBus.Publish(notification); await _dryRunInterceptor.InterceptAsync(Notify<DownloadCleanedNotification>, notification);
}
[DryRunSafeguard]
private Task Notify<T>(T message) where T: notnull
{
return _messageBus.Publish(message);
} }
private static Uri GetImageFromContext(QueueRecord record, InstanceType instanceType) => private static Uri GetImageFromContext(QueueRecord record, InstanceType instanceType) =>
@@ -32,7 +32,7 @@ public sealed class QueueCleaner : GenericHandler
LidarrClient lidarrClient, LidarrClient lidarrClient,
ArrQueueIterator arrArrQueueIterator, ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory, DownloadServiceFactory downloadServiceFactory,
NotificationPublisher notifier INotificationPublisher notifier
) : base( ) : base(
logger, downloadClientConfig, logger, downloadClientConfig,
sonarrConfig, radarrConfig, lidarrConfig, sonarrConfig, radarrConfig, lidarrConfig,
+1
View File
@@ -250,6 +250,7 @@ services:
# - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true # - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true
# - NOTIFIARR__ON_STALLED_STRIKE=true # - NOTIFIARR__ON_STALLED_STRIKE=true
# - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true # - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true
# - NOTIFIARR__ON_DOWNLOAD_CLEANED=true
# - NOTIFIARR__API_KEY=notifiarr_secret # - NOTIFIARR__API_KEY=notifiarr_secret
# - NOTIFIARR__CHANNEL_ID=discord_channel_id # - NOTIFIARR__CHANNEL_ID=discord_channel_id
volumes: volumes: