Compare commits

..

3 Commits

Author SHA1 Message Date
Flaminel
c7ad1c5ee6 fixed README typo 2025-01-11 01:45:45 +02:00
Marius Nechifor
d7913ae2b8 Add option to not use a download client (#35) 2025-01-11 01:45:12 +02:00
Marius Nechifor
21e59072d0 Increase trigger interval limit (#34) 2025-01-09 23:03:53 +02:00
15 changed files with 118 additions and 37 deletions

View File

@@ -63,12 +63,20 @@ This tool is actively developed and still a work in progress. Join the Discord s
## Using cleanuperr's blocklist (works with all supported download clients)
1. Set both `QUEUECLEANER_ENABLED` and `CONTENTBLOCKER_ENABLED` to `true` in your environment variables.
1. Set both `QUEUECLEANER__ENABLED` and `CONTENTBLOCKER_ENABLED` to `true` in your environment variables.
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Environment variables](#Environment-variables) section.
3. Once configured, cleanuperr will perform the following tasks:
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
## Using cleanuperr just for failed *arr imports (works for Usenet users as well)
1. Set `QUEUECLEANER__ENABLED` to `true`.
2. Set `QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES` to a desired value.
3. Set `DOWNLOAD_CLIENT` to `none`.
**No other action involving a download client would work (e.g. content blocking, removing stalled downloads etc.).**
## Usage
### Docker compose yaml
@@ -113,6 +121,8 @@ services:
# - TRANSMISSION__URL=http://localhost:9091
# - TRANSMISSION__USERNAME=test
# - TRANSMISSION__PASSWORD=testing
# OR
# - DOWNLOAD_CLIENT=none
- SONARR__ENABLED=true
- SONARR__SEARCHTYPE=Episode
@@ -139,8 +149,8 @@ services:
| LOGGING__FILE__PATH | No | Directory where to save the log files | empty |
| LOGGING__ENHANCED | No | Enhance logs whenever possible<br>A more detailed description is provided [here](variables.md#LOGGING__ENHANCED) | true |
|||||
| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 1h interval | 0 0/5 * * * ? |
| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 1h interval | 0 0/5 * * * ? |
| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 6h interval<br>**Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`** | 0 0/5 * * * ? |
| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)<br>Can be a max of 6h interval | 0 0/5 * * * ? |
|||||
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true |
| QUEUECLEANER__RUNSEQUENTIALLY | No | If set to true, the queue cleaner will run after the content blocker instead of running in parallel, streamlining the cleaning process | true |
@@ -153,7 +163,7 @@ services:
| CONTENTBLOCKER__WHITELIST__ENABLED | Yes if content blocker is enabled and blacklist is not enabled | Enable or disable the whitelist | false |
| CONTENTBLOCKER__WHITELIST__PATH | Yes if whitelist is enabled | Path to the whitelist (local file or url)<br>Needs to be json compatible | empty |
|||||
| DOWNLOAD_CLIENT | No | Download client that is used by *arrs<br>Can be `qbittorrent`, `deluge` or `transmission` | `qbittorrent` |
| DOWNLOAD_CLIENT | No | Download client that is used by *arrs<br>Can be `qbittorrent`, `deluge`, `transmission` or `none` | `qbittorrent` |
| QBITTORRENT__URL | No | qBittorrent instance url | http://localhost:8112 |
| QBITTORRENT__USERNAME | No | qBittorrent user | empty |
| QBITTORRENT__PASSWORD | No | qBittorrent password | empty |

View File

@@ -0,0 +1,9 @@
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.DownloadClient;
public sealed record DownloadClientConfig
{
[ConfigurationKeyName("DOWNLOAD_CLIENT")]
public Enums.DownloadClient DownloadClient { get; init; } = Enums.DownloadClient.QBittorrent;
}

View File

@@ -1,6 +0,0 @@
namespace Common.Configuration;
public static class EnvironmentVariables
{
public const string DownloadClient = "DOWNLOAD_CLIENT";
}

View File

@@ -1,8 +1,9 @@
namespace Domain.Enums;
namespace Common.Enums;
public enum DownloadClient
{
QBittorrent,
Deluge,
Transmission
Transmission,
None
}

View File

@@ -0,0 +1,7 @@
namespace Common.Helpers;
public static class Constants
{
public static readonly TimeSpan TriggerMaxLimit = TimeSpan.FromHours(6);
public static readonly TimeSpan CacheLimitBuffer = TimeSpan.FromHours(2);
}

View File

@@ -0,0 +1,6 @@
namespace Common.Helpers;
public static class StaticConfiguration
{
public static TimeSpan TriggerValue { get; set; } = TimeSpan.Zero;
}

View File

@@ -14,6 +14,7 @@ public static class ConfigurationDI
services
.Configure<QueueCleanerConfig>(configuration.GetSection(QueueCleanerConfig.SectionName))
.Configure<ContentBlockerConfig>(configuration.GetSection(ContentBlockerConfig.SectionName))
.Configure<DownloadClientConfig>(configuration)
.Configure<QBitConfig>(configuration.GetSection(QBitConfig.SectionName))
.Configure<DelugeConfig>(configuration.GetSection(DelugeConfig.SectionName))
.Configure<TransmissionConfig>(configuration.GetSection(TransmissionConfig.SectionName))

View File

@@ -1,6 +1,7 @@
using Common.Configuration;
using Common.Configuration.ContentBlocker;
using Common.Configuration.QueueCleaner;
using Common.Helpers;
using Executable.Jobs;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.Jobs;
@@ -96,18 +97,24 @@ public static class QuartzDI
return;
}
var triggerObj = (IOperableTrigger)TriggerBuilder.Create()
IOperableTrigger triggerObj = (IOperableTrigger)TriggerBuilder.Create()
.WithIdentity("ExampleTrigger")
.StartNow()
.WithCronSchedule(trigger)
.Build();
var nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
IReadOnlyList<DateTimeOffset> nextFireTimes = TriggerUtils.ComputeFireTimes(triggerObj, null, 2);
TimeSpan triggerValue = nextFireTimes[1] - nextFireTimes[0];
if (nextFireTimes[1] - nextFireTimes[0] > TimeSpan.FromHours(1))
if (triggerValue > Constants.TriggerMaxLimit)
{
throw new Exception($"{trigger} should have a fire time of maximum 1 hour");
}
if (triggerValue > StaticConfiguration.TriggerValue)
{
StaticConfiguration.TriggerValue = triggerValue;
}
q.AddTrigger(opts =>
{

View File

@@ -18,6 +18,7 @@ public static class ServicesDI
.AddTransient<QueueCleaner>()
.AddTransient<ContentBlocker>()
.AddTransient<FilenameEvaluator>()
.AddTransient<DummyDownloadService>()
.AddTransient<QBitService>()
.AddTransient<DelugeService>()
.AddTransient<TransmissionService>()

View File

@@ -1,5 +1,5 @@
using Common.Configuration;
using Common.Configuration.Arr;
using Common.Configuration.Arr;
using Common.Configuration.DownloadClient;
using Domain.Enums;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
@@ -16,6 +16,7 @@ public sealed class ContentBlocker : GenericHandler
public ContentBlocker(
ILogger<ContentBlocker> logger,
IOptions<DownloadClientConfig> downloadClientConfig,
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig,
SonarrClient sonarrClient,
@@ -23,13 +24,19 @@ public sealed class ContentBlocker : GenericHandler
ArrQueueIterator arrArrQueueIterator,
BlocklistProvider blocklistProvider,
DownloadServiceFactory downloadServiceFactory
) : base(logger, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
) : base(logger, downloadClientConfig, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
{
_blocklistProvider = blocklistProvider;
}
public override async Task ExecuteAsync()
{
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
{
_logger.LogWarning("download client is set to none");
return;
}
await _blocklistProvider.LoadBlocklistAsync();
await base.ExecuteAsync();
}

View File

@@ -1,9 +1,7 @@
using Common.Configuration;
using Common.Configuration.DownloadClient;
using Common.Configuration.DownloadClient;
using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.DownloadClient.QBittorrent;
using Infrastructure.Verticals.DownloadClient.Transmission;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@@ -12,24 +10,21 @@ namespace Infrastructure.Verticals.DownloadClient;
public sealed class DownloadServiceFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly Domain.Enums.DownloadClient _downloadClient;
private readonly Common.Enums.DownloadClient _downloadClient;
public DownloadServiceFactory(IServiceProvider serviceProvider, IConfiguration configuration)
public DownloadServiceFactory(IServiceProvider serviceProvider, IOptions<DownloadClientConfig> downloadClientConfig)
{
_serviceProvider = serviceProvider;
_downloadClient = (Domain.Enums.DownloadClient)Enum.Parse(
typeof(Domain.Enums.DownloadClient),
configuration[EnvironmentVariables.DownloadClient] ?? Domain.Enums.DownloadClient.QBittorrent.ToString(),
true
);
_downloadClient = downloadClientConfig.Value.DownloadClient;
}
public IDownloadService CreateDownloadClient() =>
_downloadClient switch
{
Domain.Enums.DownloadClient.QBittorrent => _serviceProvider.GetRequiredService<QBitService>(),
Domain.Enums.DownloadClient.Deluge => _serviceProvider.GetRequiredService<DelugeService>(),
Domain.Enums.DownloadClient.Transmission => _serviceProvider.GetRequiredService<TransmissionService>(),
Common.Enums.DownloadClient.QBittorrent => _serviceProvider.GetRequiredService<QBitService>(),
Common.Enums.DownloadClient.Deluge => _serviceProvider.GetRequiredService<DelugeService>(),
Common.Enums.DownloadClient.Transmission => _serviceProvider.GetRequiredService<TransmissionService>(),
Common.Enums.DownloadClient.None => _serviceProvider.GetRequiredService<DummyDownloadService>(),
_ => throw new ArgumentOutOfRangeException()
};
}

View File

@@ -0,0 +1,33 @@
using Common.Configuration.QueueCleaner;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.DownloadClient;
public sealed class DummyDownloadService : DownloadServiceBase
{
public DummyDownloadService(ILogger<DownloadServiceBase> logger, IOptions<QueueCleanerConfig> queueCleanerConfig, FilenameEvaluator filenameEvaluator, Striker striker) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
{
}
public override void Dispose()
{
}
public override Task LoginAsync()
{
return Task.CompletedTask;
}
public override Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
{
throw new NotImplementedException();
}
public override Task BlockUnwantedFilesAsync(string hash)
{
throw new NotImplementedException();
}
}

View File

@@ -1,4 +1,5 @@
using Domain.Enums;
using Common.Helpers;
using Domain.Enums;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
@@ -15,7 +16,7 @@ public class Striker
_logger = logger;
_cache = cache;
_cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromHours(2));
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
}
public bool StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType)

View File

@@ -1,17 +1,19 @@
using Common.Configuration.Arr;
using Common.Configuration.DownloadClient;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.DownloadClient;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.Jobs;
public abstract class GenericHandler : IDisposable
{
protected readonly ILogger<GenericHandler> _logger;
protected readonly DownloadClientConfig _downloadClientConfig;
protected readonly SonarrConfig _sonarrConfig;
protected readonly RadarrConfig _radarrConfig;
protected readonly SonarrClient _sonarrClient;
@@ -21,6 +23,7 @@ public abstract class GenericHandler : IDisposable
protected GenericHandler(
ILogger<GenericHandler> logger,
IOptions<DownloadClientConfig> downloadClientConfig,
SonarrConfig sonarrConfig,
RadarrConfig radarrConfig,
SonarrClient sonarrClient,
@@ -30,6 +33,7 @@ public abstract class GenericHandler : IDisposable
)
{
_logger = logger;
_downloadClientConfig = downloadClientConfig.Value;
_sonarrConfig = sonarrConfig;
_radarrConfig = radarrConfig;
_sonarrClient = sonarrClient;

View File

@@ -1,5 +1,5 @@
using Common.Configuration.Arr;
using Common.Configuration.QueueCleaner;
using Common.Configuration.DownloadClient;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
@@ -15,13 +15,14 @@ public sealed class QueueCleaner : GenericHandler
{
public QueueCleaner(
ILogger<QueueCleaner> logger,
IOptions<DownloadClientConfig> downloadClientConfig,
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig,
SonarrClient sonarrClient,
RadarrClient radarrClient,
ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory
) : base(logger, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
) : base(logger, downloadClientConfig, sonarrConfig.Value, radarrConfig.Value, sonarrClient, radarrClient, arrArrQueueIterator, downloadServiceFactory)
{
}
@@ -56,7 +57,11 @@ public sealed class QueueCleaner : GenericHandler
continue;
}
if (!arrClient.ShouldRemoveFromQueue(record) && !await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId))
bool shouldRemoveFromArr = arrClient.ShouldRemoveFromQueue(record);
bool shouldRemoveFromDownloadClient = _downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None &&
await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
if (!shouldRemoveFromArr && !shouldRemoveFromDownloadClient)
{
_logger.LogInformation("skip | {title}", record.Title);
continue;