mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-03 11:28:02 -05:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
baf6a8c2f4 | ||
|
|
cd345afc54 | ||
|
|
246ec4d6eb | ||
|
|
569eeae181 | ||
|
|
5a0ef56074 | ||
|
|
09bd4321fb | ||
|
|
4939e37210 | ||
|
|
9463d7587f | ||
|
|
7d2bf41bec | ||
|
|
93bb8cc18d | ||
|
|
449d9e623f | ||
|
|
3a50d9be3c |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -4,9 +4,11 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: flmorg/universal-workflows/.github/workflows/dotnet.build.app.yml@main
|
||||
uses: flmorg/universal-workflows-testing/.github/workflows/dotnet.build.app.yml@main
|
||||
with:
|
||||
dockerRepository: flaminel/cleanuperr
|
||||
githubContext: ${{ toJSON(github) }}
|
||||
outputName: cleanuperr
|
||||
selfContained: false
|
||||
baseImage: 9.0-bookworm-slim
|
||||
secrets: inherit
|
||||
26
README.md
26
README.md
@@ -2,7 +2,7 @@ _Love this project? Give it a ⭐️ and let others know!_
|
||||
|
||||
# <img width="24px" src="./Logo/256.png" alt="cleanuperr"></img> Cleanuperr
|
||||
|
||||
[](https://discord.gg/sWggpnmGNY)
|
||||
[](https://discord.gg/SCtMCgtsc4)
|
||||
|
||||
Cleanuperr is a tool for automating the cleanup of unwanted or blocked files in Sonarr, Radarr, and supported download clients like qBittorrent. It removes incomplete or blocked downloads, updates queues, and enforces blacklists or whitelists to manage file selection. After removing blocked content, Cleanuperr can also trigger a search to replace the deleted shows/movies.
|
||||
|
||||
@@ -15,17 +15,37 @@ Cleanuperr was created primarily to address malicious files, such as `*.lnk` or
|
||||
> - Remove and block downloads that have a low download speed or high estimated completion time.
|
||||
> - Remove downloads blocked by qBittorrent or by Cleanuperr's **content blocker**.
|
||||
> - Trigger a search for downloads removed from the *arrs.
|
||||
> - Remove downloads that have been seeding for a certain amount of time.
|
||||
> - Remove downloads that have no hardlinks (have been upgraded by the *arrs).
|
||||
> - Clean up downloads that have been seeding for a certain amount of time.
|
||||
> - Notify on strike or download removal.
|
||||
> - Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuperr.
|
||||
|
||||
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.
|
||||
|
||||
## Quick Start
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> 1. **Docker (Recommended)**
|
||||
> Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`.
|
||||
>
|
||||
> 2. **Unraid (for Unraid users)**
|
||||
> Use the Unraid Community App.
|
||||
>
|
||||
> 3. **Manual Installation (if you're not using Docker)**
|
||||
> Go to [Windows](#windows), [Linux](#linux) or [MacOS](#macos).
|
||||
|
||||
# Docs
|
||||
|
||||
Docs can be found [here](https://flmorg.github.io/cleanuperr/).
|
||||
|
||||
# <img style="vertical-align: middle;" width="24px" src="./Logo/256.png" alt="Cleanuperr"> <span style="vertical-align: middle;">Cleanuperr</span> <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/solid/x.svg" height="24px" width="30px" style="vertical-align: middle;"> <span style="vertical-align: middle;">Huntarr</span> <img style="vertical-align: middle;" width="24px" src="https://github.com/plexguide/Huntarr.io/blob/main/frontend/static/logo/512.png?raw=true" alt Huntarr></img>
|
||||
|
||||
Think of **Cleanuperr** as the janitor of your server; it keeps your download queue spotless, removes clutter, and blocks malicious files. Now imagine combining that with **Huntarr**, the compulsive librarian who finds missing and upgradable media to complete your collection
|
||||
|
||||
While **Huntarr** fills in the blanks and improves what you already have, **Cleanuperr** makes sure that only clean downloads get through. If you're aiming for a reliable and self-sufficient setup, **Cleanuperr** and **Huntarr** will take your automated media stack to another level.
|
||||
|
||||
<span style="font-size:24px"> ➡️ [**Huntarr**](https://github.com/plexguide/Huntarr.io) <span style="vertical-align: middle"></span></span>
|
||||
|
||||
# Credits
|
||||
Special thanks for inspiration go to:
|
||||
- [ThijmenGThN/swaparr](https://github.com/ThijmenGThN/swaparr)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Common.Configuration.Arr;
|
||||
|
||||
@@ -7,6 +8,9 @@ public abstract record ArrConfig
|
||||
public required bool Enabled { get; init; }
|
||||
|
||||
public Block Block { get; init; } = new();
|
||||
|
||||
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
|
||||
public short ImportFailedMaxStrikes { get; init; } = -1;
|
||||
|
||||
public required List<ArrInstance> Instances { get; init; }
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||
[ConfigurationKeyName("UNLINKED_TARGET_CATEGORY")]
|
||||
public string UnlinkedTargetCategory { get; init; } = "cleanuperr-unlinked";
|
||||
|
||||
[ConfigurationKeyName("UNLINKED_USE_TAG")]
|
||||
public bool UnlinkedUseTag { get; init; }
|
||||
|
||||
[ConfigurationKeyName("UNLINKED_IGNORED_ROOT_DIR")]
|
||||
public string UnlinkedIgnoredRootDir { get; init; } = string.Empty;
|
||||
|
||||
@@ -33,11 +36,6 @@ public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||
return;
|
||||
}
|
||||
|
||||
if (Categories?.Count is null or 0)
|
||||
{
|
||||
throw new ValidationException("no categories configured");
|
||||
}
|
||||
|
||||
if (Categories?.GroupBy(x => x.Name).Any(x => x.Count() > 1) is true)
|
||||
{
|
||||
throw new ValidationException("duplicated clean categories found");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Common.Exceptions;
|
||||
using Common.Enums;
|
||||
using Common.Exceptions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Common.Configuration.General;
|
||||
@@ -10,6 +11,9 @@ public sealed record HttpConfig : IConfig
|
||||
|
||||
[ConfigurationKeyName("HTTP_TIMEOUT")]
|
||||
public ushort Timeout { get; init; } = 100;
|
||||
|
||||
[ConfigurationKeyName("HTTP_VALIDATE_CERT")]
|
||||
public CertificateValidationType CertificateValidation { get; init; } = CertificateValidationType.Enabled;
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
|
||||
8
code/Common/Enums/CertificateValidationType.cs
Normal file
8
code/Common/Enums/CertificateValidationType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Common.Enums;
|
||||
|
||||
public enum CertificateValidationType
|
||||
{
|
||||
Enabled = 0,
|
||||
DisabledForLocalAddresses = 1,
|
||||
Disabled = 2
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Net;
|
||||
using Common.Configuration.General;
|
||||
using Common.Helpers;
|
||||
using Infrastructure.Services;
|
||||
using Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
using Infrastructure.Verticals.Notifications.Consumers;
|
||||
using Infrastructure.Verticals.Notifications.Models;
|
||||
using MassTransit;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Polly;
|
||||
using Polly.Extensions.Http;
|
||||
|
||||
@@ -62,6 +64,15 @@ public static class MainDI
|
||||
{
|
||||
x.Timeout = TimeSpan.FromSeconds(config.Timeout);
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(provider =>
|
||||
{
|
||||
CertificateValidationService service = provider.GetRequiredService<CertificateValidationService>();
|
||||
|
||||
return new HttpClientHandler
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = service.ShouldByPassValidationError
|
||||
};
|
||||
})
|
||||
.AddRetryPolicyHandler(config);
|
||||
|
||||
// add Deluge HttpClient
|
||||
|
||||
@@ -3,6 +3,7 @@ using Common.Configuration.DownloadCleaner;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Infrastructure.Interceptors;
|
||||
using Infrastructure.Providers;
|
||||
using Infrastructure.Services;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.DownloadCleaner;
|
||||
@@ -21,6 +22,7 @@ public static class ServicesDI
|
||||
public static IServiceCollection AddServices(this IServiceCollection services) =>
|
||||
services
|
||||
.AddTransient<IDryRunInterceptor, DryRunInterceptor>()
|
||||
.AddTransient<CertificateValidationService>()
|
||||
.AddTransient<SonarrClient>()
|
||||
.AddTransient<RadarrClient>()
|
||||
.AddTransient<LidarrClient>()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"DRY_RUN": true,
|
||||
"HTTP_MAX_RETRIES": 0,
|
||||
"HTTP_TIMEOUT": 10,
|
||||
"HTTP_TIMEOUT": 100,
|
||||
"HTTP_VALIDATE_CERT": "enabled",
|
||||
"Logging": {
|
||||
"LogLevel": "Verbose",
|
||||
"Enhanced": true,
|
||||
@@ -56,6 +57,7 @@
|
||||
}
|
||||
],
|
||||
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
|
||||
"UNLINKED_USE_TAG": false,
|
||||
"UNLINKED_IGNORED_ROOT_DIR": "",
|
||||
"UNLINKED_CATEGORIES": [
|
||||
"tv-sonarr",
|
||||
@@ -83,6 +85,7 @@
|
||||
},
|
||||
"Sonarr": {
|
||||
"Enabled": true,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"SearchType": "Episode",
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
@@ -97,6 +100,7 @@
|
||||
},
|
||||
"Radarr": {
|
||||
"Enabled": true,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||
@@ -110,6 +114,7 @@
|
||||
},
|
||||
"Lidarr": {
|
||||
"Enabled": true,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"DRY_RUN": false,
|
||||
"HTTP_MAX_RETRIES": 0,
|
||||
"HTTP_TIMEOUT": 100,
|
||||
"HTTP_VALIDATE_CERT": "enabled",
|
||||
"Logging": {
|
||||
"LogLevel": "Information",
|
||||
"Enhanced": true,
|
||||
@@ -21,7 +22,7 @@
|
||||
"IGNORED_DOWNLOADS_PATH": ""
|
||||
},
|
||||
"QueueCleaner": {
|
||||
"Enabled": true,
|
||||
"Enabled": false,
|
||||
"RunSequentially": true,
|
||||
"IGNORED_DOWNLOADS_PATH": "",
|
||||
"IMPORT_FAILED_MAX_STRIKES": 0,
|
||||
@@ -32,13 +33,21 @@
|
||||
"STALLED_RESET_STRIKES_ON_PROGRESS": false,
|
||||
"STALLED_IGNORE_PRIVATE": false,
|
||||
"STALLED_DELETE_PRIVATE": false,
|
||||
"DOWNLOADING_METADATA_MAX_STRIKES": 0
|
||||
"DOWNLOADING_METADATA_MAX_STRIKES": 0,
|
||||
"SLOW_MAX_STRIKES": 0,
|
||||
"SLOW_RESET_STRIKES_ON_PROGRESS": true,
|
||||
"SLOW_IGNORE_PRIVATE": false,
|
||||
"SLOW_DELETE_PRIVATE": false,
|
||||
"SLOW_MIN_SPEED": "",
|
||||
"SLOW_MAX_TIME": 0,
|
||||
"SLOW_IGNORE_ABOVE_SIZE": ""
|
||||
},
|
||||
"DownloadCleaner": {
|
||||
"Enabled": false,
|
||||
"DELETE_PRIVATE": false,
|
||||
"CATEGORIES": [],
|
||||
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
|
||||
"UNLINKED_USE_TAG": false,
|
||||
"UNLINKED_IGNORED_ROOT_DIR": "",
|
||||
"UNLINKED_CATEGORIES": [],
|
||||
"IGNORED_DOWNLOADS_PATH": ""
|
||||
@@ -63,6 +72,7 @@
|
||||
},
|
||||
"Sonarr": {
|
||||
"Enabled": false,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"SearchType": "Episode",
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
@@ -77,6 +87,7 @@
|
||||
},
|
||||
"Radarr": {
|
||||
"Enabled": false,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
"Path": ""
|
||||
@@ -90,6 +101,7 @@
|
||||
},
|
||||
"Lidarr": {
|
||||
"Enabled": false,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
"Path": ""
|
||||
|
||||
55
code/Infrastructure/Extensions/IpAddressExtensions.cs
Normal file
55
code/Infrastructure/Extensions/IpAddressExtensions.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
public static class IpAddressExtensions
|
||||
{
|
||||
public static bool IsLocalAddress(this IPAddress ipAddress)
|
||||
{
|
||||
// Map back to IPv4 if mapped to IPv6, for example "::ffff:1.2.3.4" to "1.2.3.4".
|
||||
if (ipAddress.IsIPv4MappedToIPv6)
|
||||
{
|
||||
ipAddress = ipAddress.MapToIPv4();
|
||||
}
|
||||
|
||||
// Checks loopback ranges for both IPv4 and IPv6.
|
||||
if (IPAddress.IsLoopback(ipAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// IPv4
|
||||
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
return IsLocalIPv4(ipAddress.GetAddressBytes());
|
||||
}
|
||||
|
||||
// IPv6
|
||||
if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
return ipAddress.IsIPv6LinkLocal ||
|
||||
ipAddress.IsIPv6UniqueLocal ||
|
||||
ipAddress.IsIPv6SiteLocal;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsLocalIPv4(byte[] ipv4Bytes)
|
||||
{
|
||||
// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
|
||||
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
|
||||
|
||||
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
|
||||
bool IsClassA() => ipv4Bytes[0] == 10;
|
||||
|
||||
// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
|
||||
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;
|
||||
|
||||
// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
|
||||
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;
|
||||
|
||||
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
<PackageReference Include="MassTransit" Version="8.3.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.2" />
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
|
||||
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||
<PackageReference Include="Scrutor" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
86
code/Infrastructure/Services/CertificateValidationService.cs
Normal file
86
code/Infrastructure/Services/CertificateValidationService.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Net;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Common.Configuration.General;
|
||||
using Common.Enums;
|
||||
using Infrastructure.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Services;
|
||||
|
||||
public class CertificateValidationService
|
||||
{
|
||||
private readonly ILogger<CertificateValidationService> _logger;
|
||||
private readonly HttpConfig _config;
|
||||
|
||||
public CertificateValidationService(ILogger<CertificateValidationService> logger, IOptions<HttpConfig> config)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
}
|
||||
|
||||
public bool ShouldByPassValidationError(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors)
|
||||
{
|
||||
var targetHostName = string.Empty;
|
||||
|
||||
if (sender is not SslStream && sender is not string)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sender is SslStream request)
|
||||
{
|
||||
targetHostName = request.TargetHostName;
|
||||
}
|
||||
|
||||
// Mailkit passes host in sender as string
|
||||
if (sender is string stringHost)
|
||||
{
|
||||
targetHostName = stringHost;
|
||||
}
|
||||
|
||||
if (certificate is X509Certificate2 cert2 && cert2.SignatureAlgorithm.FriendlyName == "md5RSA")
|
||||
{
|
||||
_logger.LogError(
|
||||
$"https://{targetHostName} uses the obsolete md5 hash in its https certificate, if that is your certificate, please (re)create certificate with better algorithm as soon as possible.");
|
||||
}
|
||||
|
||||
if (sslPolicyErrors == SslPolicyErrors.None)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (targetHostName is "localhost" or "127.0.0.1")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var ipAddresses = GetIpAddresses(targetHostName);
|
||||
|
||||
if (_config.CertificateValidation == CertificateValidationType.Disabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_config.CertificateValidation == CertificateValidationType.DisabledForLocalAddresses &&
|
||||
ipAddresses.All(i => i.IsLocalAddress()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.LogError($"certificate validation for {targetHostName} failed. {sslPolicyErrors}");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IPAddress[] GetIpAddresses(string host)
|
||||
{
|
||||
if (IPAddress.TryParse(host, out var ipAddress))
|
||||
{
|
||||
return [ipAddress];
|
||||
}
|
||||
|
||||
return Dns.GetHostEntry(host).AddressList;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public abstract class ArrClient : IArrClient
|
||||
return queueResponse;
|
||||
}
|
||||
|
||||
public virtual async Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload)
|
||||
public virtual async Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes)
|
||||
{
|
||||
if (_queueCleanerConfig.ImportFailedIgnorePrivate && isPrivateDownload)
|
||||
{
|
||||
@@ -102,11 +102,19 @@ public abstract class ArrClient : IArrClient
|
||||
_logger.LogDebug("skip failed import check | contains ignored pattern | {name}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arrMaxStrikes is 0)
|
||||
{
|
||||
_logger.LogDebug("skip failed import check | arr max strikes is 0 | {name}", record.Title);
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort maxStrikes = arrMaxStrikes > 0 ? (ushort)arrMaxStrikes : _queueCleanerConfig.ImportFailedMaxStrikes;
|
||||
|
||||
return await _striker.StrikeAndCheckLimit(
|
||||
record.DownloadId,
|
||||
record.Title,
|
||||
_queueCleanerConfig.ImportFailedMaxStrikes,
|
||||
maxStrikes,
|
||||
StrikeType.ImportFailed
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ public interface IArrClient
|
||||
{
|
||||
Task<QueueListResponse> GetQueueItemsAsync(ArrInstance arrInstance, int page);
|
||||
|
||||
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload);
|
||||
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes);
|
||||
|
||||
Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, DeleteReason deleteReason);
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ public sealed class ContentBlocker : GenericHandler
|
||||
await base.ExecuteAsync();
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config)
|
||||
{
|
||||
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||
|
||||
|
||||
@@ -58,9 +58,12 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
return;
|
||||
}
|
||||
|
||||
if (_config.Categories?.Count is null or 0)
|
||||
bool isUnlinkedEnabled = !string.IsNullOrEmpty(_config.UnlinkedTargetCategory) && _config.UnlinkedCategories?.Count > 0;
|
||||
bool isCleaningEnabled = _config.Categories?.Count > 0;
|
||||
|
||||
if (!isUnlinkedEnabled && !isCleaningEnabled)
|
||||
{
|
||||
_logger.LogWarning("no categories configured");
|
||||
_logger.LogWarning("{name} is not configured properly", nameof(DownloadCleaner));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,15 +71,27 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
|
||||
await _downloadService.LoginAsync();
|
||||
List<object>? downloads = await _downloadService.GetSeedingDownloads();
|
||||
List<object>? downloadsToChangeCategory = null;
|
||||
|
||||
if (downloads?.Count is null or 0)
|
||||
{
|
||||
_logger.LogDebug("no seeding downloads found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_config.UnlinkedTargetCategory) && _config.UnlinkedCategories?.Count > 0)
|
||||
_logger.LogTrace("found {count} seeding downloads", downloads.Count);
|
||||
|
||||
List<object>? downloadsToChangeCategory = null;
|
||||
|
||||
if (isUnlinkedEnabled)
|
||||
{
|
||||
if (!_hardLinkCategoryCreated)
|
||||
{
|
||||
_logger.LogDebug("creating category {cat}", _config.UnlinkedTargetCategory);
|
||||
|
||||
await _downloadService.CreateCategoryAsync(_config.UnlinkedTargetCategory);
|
||||
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.QBittorrent && !_config.UnlinkedUseTag)
|
||||
{
|
||||
_logger.LogDebug("creating category {cat}", _config.UnlinkedTargetCategory);
|
||||
await _downloadService.CreateCategoryAsync(_config.UnlinkedTargetCategory);
|
||||
}
|
||||
|
||||
_hardLinkCategoryCreated = true;
|
||||
}
|
||||
|
||||
@@ -89,20 +104,30 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr, true);
|
||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr, true);
|
||||
await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr, true);
|
||||
|
||||
if (isUnlinkedEnabled)
|
||||
{
|
||||
_logger.LogTrace("found {count} potential downloads to change category", downloadsToChangeCategory?.Count);
|
||||
await _downloadService.ChangeCategoryForNoHardLinksAsync(downloadsToChangeCategory, _excludedHashes, ignoredDownloads);
|
||||
_logger.LogTrace("finished changing category");
|
||||
}
|
||||
|
||||
_logger.LogTrace("looking for downloads to change category");
|
||||
await _downloadService.ChangeCategoryForNoHardLinksAsync(downloadsToChangeCategory, _excludedHashes, ignoredDownloads);
|
||||
if (_config.Categories?.Count is null or 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<object>? downloadsToClean = _downloadService.FilterDownloadsToBeCleanedAsync(downloads, _config.Categories);
|
||||
|
||||
// release unused objects
|
||||
downloads = null;
|
||||
|
||||
_logger.LogTrace("looking for downloads to clean");
|
||||
|
||||
_logger.LogTrace("found {count} potential downloads to clean", downloadsToClean?.Count);
|
||||
await _downloadService.CleanDownloadsAsync(downloadsToClean, _config.Categories, _excludedHashes, ignoredDownloads);
|
||||
_logger.LogTrace("finished cleaning downloads");
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config)
|
||||
{
|
||||
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
||||
|
||||
|
||||
@@ -251,6 +251,15 @@ public class QBitService : DownloadService, IQBitService
|
||||
?.Cast<TorrentInfo>()
|
||||
.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Where(x => categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.Where(x =>
|
||||
{
|
||||
if (_downloadCleanerConfig.UnlinkedUseTag)
|
||||
{
|
||||
return !x.Tags.Any(tag => tag.Equals(_downloadCleanerConfig.UnlinkedTargetCategory, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
|
||||
@@ -436,12 +445,18 @@ public class QBitService : DownloadService, IQBitService
|
||||
}
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(ChangeCategory, download.Hash, _downloadCleanerConfig.UnlinkedTargetCategory);
|
||||
|
||||
if (_downloadCleanerConfig.UnlinkedUseTag)
|
||||
{
|
||||
_logger.LogInformation("tag added for {name}", download.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("category changed for {name}", download.Name);
|
||||
download.Category = _downloadCleanerConfig.UnlinkedTargetCategory;
|
||||
}
|
||||
|
||||
_logger.LogInformation("category changed for {name}", download.Name);
|
||||
|
||||
await _notifier.NotifyCategoryChanged(download.Category, _downloadCleanerConfig.UnlinkedTargetCategory);
|
||||
|
||||
download.Category = _downloadCleanerConfig.UnlinkedTargetCategory;
|
||||
await _notifier.NotifyCategoryChanged(download.Category, _downloadCleanerConfig.UnlinkedTargetCategory, _downloadCleanerConfig.UnlinkedUseTag);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +482,12 @@ public class QBitService : DownloadService, IQBitService
|
||||
[DryRunSafeguard]
|
||||
protected virtual async Task ChangeCategory(string hash, string newCategory)
|
||||
{
|
||||
if (_downloadCleanerConfig.UnlinkedUseTag)
|
||||
{
|
||||
await _client.AddTorrentTagAsync([hash], newCategory);
|
||||
return;
|
||||
}
|
||||
|
||||
await _client.SetTorrentCategoryAsync([hash], newCategory);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ public abstract class GenericHandler : IHandler, IDisposable
|
||||
_downloadService.Dispose();
|
||||
}
|
||||
|
||||
protected abstract Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType);
|
||||
protected abstract Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config);
|
||||
|
||||
protected async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType, bool throwOnFailure = false)
|
||||
{
|
||||
@@ -80,7 +80,7 @@ public abstract class GenericHandler : IHandler, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessInstanceAsync(arrInstance, instanceType);
|
||||
await ProcessInstanceAsync(arrInstance, instanceType, config);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
||||
@@ -10,5 +10,5 @@ public interface INotificationPublisher
|
||||
|
||||
Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason);
|
||||
|
||||
Task NotifyCategoryChanged(string oldCategory, string newCategory);
|
||||
Task NotifyCategoryChanged(string oldCategory, string newCategory, bool isTag = false);
|
||||
}
|
||||
@@ -123,21 +123,29 @@ public class NotificationPublisher : INotificationPublisher
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task NotifyCategoryChanged(string oldCategory, string newCategory)
|
||||
public virtual async Task NotifyCategoryChanged(string oldCategory, string newCategory, bool isTag = false)
|
||||
{
|
||||
CategoryChangedNotification notification = new()
|
||||
{
|
||||
Title = "Category changed",
|
||||
Title = isTag? "Tag added" : "Category changed",
|
||||
Description = ContextProvider.Get<string>("downloadName"),
|
||||
Fields =
|
||||
[
|
||||
new() { Title = "Hash", Text = ContextProvider.Get<string>("hash").ToLowerInvariant() },
|
||||
new() { Title = "Old category", Text = oldCategory },
|
||||
new() { Title = "New category", Text = newCategory }
|
||||
new() { Title = "Hash", Text = ContextProvider.Get<string>("hash").ToLowerInvariant() }
|
||||
],
|
||||
Level = NotificationLevel.Important
|
||||
};
|
||||
|
||||
if (isTag)
|
||||
{
|
||||
notification.Fields.Add(new() { Title = "Tag", Text = newCategory });
|
||||
}
|
||||
else
|
||||
{
|
||||
notification.Fields.Add(new() { Title = "Old category", Text = oldCategory });
|
||||
notification.Fields.Add(new() { Title = "New category", Text = newCategory });
|
||||
}
|
||||
|
||||
await NotifyInternal(notification);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class QueueCleaner : GenericHandler
|
||||
_ignoredDownloadsProvider = ignoredDownloadsProvider;
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType, ArrConfig config)
|
||||
{
|
||||
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||
|
||||
@@ -108,7 +108,7 @@ public sealed class QueueCleaner : GenericHandler
|
||||
}
|
||||
|
||||
// failed import check
|
||||
bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate);
|
||||
bool shouldRemoveFromArr = await arrClient.ShouldRemoveFromQueue(instanceType, record, downloadCheckResult.IsPrivate, config.ImportFailedMaxStrikes);
|
||||
DeleteReason deleteReason = downloadCheckResult.ShouldRemove ? downloadCheckResult.DeleteReason : DeleteReason.ImportFailed;
|
||||
|
||||
if (!shouldRemoveFromArr && !downloadCheckResult.ShouldRemove)
|
||||
|
||||
@@ -221,15 +221,18 @@ services:
|
||||
- DOWNLOADCLEANER__ENABLED=true
|
||||
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored
|
||||
- DOWNLOADCLEANER__DELETE_PRIVATE=false
|
||||
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME=0
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME=99999
|
||||
- DOWNLOADCLEANER__CATEGORIES__1__NAME=nohardlink
|
||||
- DOWNLOADCLEANER__CATEGORIES__1__NAME=cleanuperr-unlinked
|
||||
- DOWNLOADCLEANER__CATEGORIES__1__MAX_RATIO=-1
|
||||
- DOWNLOADCLEANER__CATEGORIES__1__MIN_SEED_TIME=0
|
||||
- DOWNLOADCLEANER__CATEGORIES__1__MAX_SEED_TIME=99999
|
||||
|
||||
- DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY=cleanuperr-unlinked
|
||||
- DOWNLOADCLEANER__UNLINKED_USE_TAG=false
|
||||
- DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR=/downloads
|
||||
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__0=tv-sonarr
|
||||
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__1=radarr
|
||||
@@ -249,6 +252,7 @@ services:
|
||||
# - TRANSMISSION__PASSWORD=testing
|
||||
|
||||
- SONARR__ENABLED=true
|
||||
- SONARR__IMPORT_FAILED_MAX_STRIKES=-1
|
||||
- SONARR__SEARCHTYPE=Episode
|
||||
- SONARR__BLOCK__TYPE=blacklist
|
||||
- SONARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
||||
@@ -256,12 +260,14 @@ services:
|
||||
- SONARR__INSTANCES__0__APIKEY=425d1e713f0c405cbbf359ac0502c1f4
|
||||
|
||||
- RADARR__ENABLED=true
|
||||
- RADARR__IMPORT_FAILED_MAX_STRIKES=-1
|
||||
- RADARR__BLOCK__TYPE=blacklist
|
||||
- RADARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist
|
||||
- RADARR__INSTANCES__0__URL=http://radarr:7878
|
||||
- RADARR__INSTANCES__0__APIKEY=8b7454f668e54c5b8f44f56f93969761
|
||||
|
||||
- LIDARR__ENABLED=true
|
||||
- LIDARR__IMPORT_FAILED_MAX_STRIKES=-1
|
||||
- LIDARR__BLOCK__TYPE=blacklist
|
||||
- LIDARR__BLOCK__PATH=https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist # TODO
|
||||
- LIDARR__INSTANCES__0__URL=http://lidarr:8686
|
||||
|
||||
@@ -13,7 +13,7 @@ Cleanuperr was created primarily to address malicious files, such as `*.lnk` or
|
||||
<Warning>
|
||||
Because this tool is actively developed and still a work in progress, 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
|
||||
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/SCtMCgtsc4
|
||||
</Warning>
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ sidebar_position: 3
|
||||
---
|
||||
|
||||
import DownloadCleanerHardlinksSettings from '@site/src/components/configuration/download-cleaner/DownloadCleanerHardlinksSettings';
|
||||
import { Important } from '@site/src/components/Admonition';
|
||||
import { Important, Warning } from '@site/src/components/Admonition';
|
||||
|
||||
# Hardlinks Settings
|
||||
|
||||
@@ -16,4 +16,8 @@ The Download Cleaner will change the category of a download that has no hardlink
|
||||
If your download client's download directory is `/downloads`, it should be the same for Cleanuperr.
|
||||
</Important>
|
||||
|
||||
<Warning>
|
||||
While it is not needed to configure the arrs for this feature, it is recommended you do. If the arrs are not configured, downloads that are waiting to be imported might be affected by it.
|
||||
</Warning>
|
||||
|
||||
<DownloadCleanerHardlinksSettings/>
|
||||
@@ -12,8 +12,12 @@ services:
|
||||
image: ghcr.io/flmorg/cleanuperr:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# if you want persistent logs
|
||||
- ./cleanuperr/logs:/var/logs
|
||||
# if you want to ignore certain downloads from being processed
|
||||
- ./cleanuperr/ignored.txt:/ignored.txt
|
||||
# if you're using cross-seed and the hardlinks functionality
|
||||
- ./downloads:/downloads
|
||||
environment:
|
||||
# general settings
|
||||
- TZ=America/New_York
|
||||
@@ -87,6 +91,7 @@ services:
|
||||
|
||||
# change category for downloads with no hardlinks
|
||||
- DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY=cleanuperr-unlinked
|
||||
- DOWNLOADCLEANER__UNLINKED_USE_TAG=false
|
||||
- DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR=/downloads
|
||||
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__0=tv-sonarr
|
||||
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__1=radarr
|
||||
@@ -113,6 +118,7 @@ services:
|
||||
# - TRANSMISSION__PASSWORD=testing
|
||||
|
||||
- SONARR__ENABLED=true
|
||||
- SONARR__IMPORT_FAILED_MAX_STRIKES=-1
|
||||
- SONARR__SEARCHTYPE=Episode
|
||||
- SONARR__BLOCK__TYPE=blacklist
|
||||
- SONARR__BLOCK__PATH=https://example.com/path/to/file.txt
|
||||
@@ -122,6 +128,7 @@ services:
|
||||
- SONARR__INSTANCES__1__APIKEY=secret2
|
||||
|
||||
- RADARR__ENABLED=true
|
||||
- RADARR__IMPORT_FAILED_MAX_STRIKES=-1
|
||||
- RADARR__BLOCK__TYPE=blacklist
|
||||
- RADARR__BLOCK__PATH=https://example.com/path/to/file.txt
|
||||
- RADARR__INSTANCES__0__URL=http://localhost:7878
|
||||
@@ -130,6 +137,7 @@ services:
|
||||
- RADARR__INSTANCES__1__APIKEY=secret4
|
||||
|
||||
- LIDARR__ENABLED=true
|
||||
- LIDARR__IMPORT_FAILED_MAX_STRIKES=-1
|
||||
- LIDARR__BLOCK__TYPE=blacklist
|
||||
- LIDARR__BLOCK__PATH=https://example.com/path/to/file.txt
|
||||
- LIDARR__INSTANCES__0__URL=http://radarr:8686
|
||||
|
||||
@@ -78,6 +78,7 @@ import { Note } from '@site/src/components/Admonition';
|
||||
}
|
||||
],
|
||||
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
|
||||
"DOWNLOADCLEANER__UNLINKED_USE_TAG": false,
|
||||
"UNLINKED_IGNORED_ROOT_DIR": "/downloads",
|
||||
"UNLINKED_CATEGORIES": [
|
||||
"tv-sonarr",
|
||||
@@ -105,6 +106,7 @@ import { Note } from '@site/src/components/Admonition';
|
||||
},
|
||||
"Sonarr": {
|
||||
"Enabled": true,
|
||||
"IMPORT_FAILED_MAX_STRIKES=-1
|
||||
"SearchType": "Episode",
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
@@ -123,6 +125,7 @@ import { Note } from '@site/src/components/Admonition';
|
||||
},
|
||||
"Radarr": {
|
||||
"Enabled": true,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
"Path": "https://example.com/path/to/file.txt"
|
||||
@@ -140,6 +143,7 @@ import { Note } from '@site/src/components/Admonition';
|
||||
},
|
||||
"Lidarr": {
|
||||
"Enabled": true,
|
||||
"IMPORT_FAILED_MAX_STRIKES": -1,
|
||||
"Block": {
|
||||
"Type": "blacklist",
|
||||
"Path": "https://example.com/path/to/file.txt"
|
||||
|
||||
@@ -4,10 +4,13 @@ import { Note } from '@site/src/components/Admonition';
|
||||
|
||||
1. **Docker (Recommended)**
|
||||
Pull the Docker image from `ghcr.io/flmorg/cleanuperr:latest`.
|
||||
[Configuration example here.](/docs/configuration/examples/docker)
|
||||
2. **Unraid (for Unraid users)**
|
||||
Use the Unraid Community App.
|
||||
[Configuration example here.](/docs/configuration/examples/docker)
|
||||
3. **Manual Installation (if you're not using Docker)**
|
||||
Go to [Windows](/docs/installation/windows), [Linux](/docs/installation/linux) or [MacOS](/docs/installation/macos).
|
||||
[Configuration example here.](/docs/configuration/examples/config-file)
|
||||
|
||||
<Note>
|
||||
Refer to the [Configuration](/docs/category/configuration) section for detailed configuration instructions.
|
||||
|
||||
@@ -61,7 +61,7 @@ const config: Config = {
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://discord.gg/sWggpnmGNY',
|
||||
href: 'https://discord.gg/SCtMCgtsc4',
|
||||
label: 'Discord',
|
||||
position: 'right',
|
||||
}
|
||||
|
||||
@@ -81,6 +81,17 @@ const settings: EnvVarProps[] = [
|
||||
type: "positive integer number",
|
||||
defaultValue: "100",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "HTTP_VALIDATE_CERT",
|
||||
description: [
|
||||
"Controls whether to validate SSL certificates for HTTPS connections.",
|
||||
"Set to `Disabled` to ignore SSL certificate errors."
|
||||
],
|
||||
type: "text",
|
||||
defaultValue: "Enabled",
|
||||
required: false,
|
||||
acceptedValues: ["Enabled", "DisabledForLocalAddresses", "Disabled"],
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -12,6 +12,24 @@ const settings: EnvVarProps[] = [
|
||||
required: false,
|
||||
acceptedValues: ["true", "false"],
|
||||
},
|
||||
{
|
||||
name: "LIDARR__IMPORT_FAILED_MAX_STRIKES",
|
||||
description: [
|
||||
"Number of strikes before removing a failed import. Set to `0` to never remove failed imports.",
|
||||
"A strike is given when an item fails to be imported."
|
||||
],
|
||||
type: "integer number",
|
||||
defaultValue: "-1",
|
||||
required: false,
|
||||
notes: [
|
||||
"If the value is a positive number, it overwrites the values of [QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES](/cleanuperr/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES).",
|
||||
"`0` means to never remove failed imports.",
|
||||
"If not set to `0` or a negative number, the minimum value is `3`.",
|
||||
],
|
||||
warnings: [
|
||||
"The value is not restricted to be a certain positive number. Use a low value (e.g. `1`) at your own risk."
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "LIDARR__BLOCK__TYPE",
|
||||
description: [
|
||||
|
||||
@@ -12,6 +12,24 @@ const settings: EnvVarProps[] = [
|
||||
required: false,
|
||||
acceptedValues: ["true", "false"],
|
||||
},
|
||||
{
|
||||
name: "RADARR__IMPORT_FAILED_MAX_STRIKES",
|
||||
description: [
|
||||
"Number of strikes before removing a failed import. Set to `0` to never remove failed imports.",
|
||||
"A strike is given when an item fails to be imported."
|
||||
],
|
||||
type: "integer number",
|
||||
defaultValue: "-1",
|
||||
required: false,
|
||||
notes: [
|
||||
"If the value is a positive number, it overwrites the values of [QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES](/cleanuperr/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES).",
|
||||
"`0` means to never remove failed imports.",
|
||||
"If not set to `0` or a negative number, the minimum value is `3`.",
|
||||
],
|
||||
warnings: [
|
||||
"The value is not restricted to be a certain positive number. Use a low value (e.g. `1`) at your own risk."
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "RADARR__BLOCK__TYPE",
|
||||
description: [
|
||||
|
||||
@@ -12,6 +12,24 @@ const settings: EnvVarProps[] = [
|
||||
required: false,
|
||||
acceptedValues: ["true", "false"],
|
||||
},
|
||||
{
|
||||
name: "SONARR__IMPORT_FAILED_MAX_STRIKES",
|
||||
description: [
|
||||
"Number of strikes before removing a failed import. Set to `0` to never remove failed imports.",
|
||||
"A strike is given when an item fails to be imported."
|
||||
],
|
||||
type: "integer number",
|
||||
defaultValue: "-1",
|
||||
required: false,
|
||||
notes: [
|
||||
"If the value is a positive number, it overwrites the values of [QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES](/cleanuperr/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES).",
|
||||
"`0` means to never remove failed imports.",
|
||||
"If not set to `0` or a negative number, the minimum value is `3`.",
|
||||
],
|
||||
warnings: [
|
||||
"The value is not restricted to be a certain positive number. Use a low value (e.g. `1`) at your own risk."
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "SONARR__BLOCK__TYPE",
|
||||
description: [
|
||||
|
||||
@@ -11,6 +11,20 @@ const settings: EnvVarProps[] = [
|
||||
defaultValue: "cleanuperr-unlinked",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: "DOWNLOADCLEANER__UNLINKED_USE_TAG",
|
||||
description: [
|
||||
"If set to true, a tag will be set instead of changing the category.",
|
||||
],
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
required: false,
|
||||
acceptedValues: ["true", "false"],
|
||||
notes: [
|
||||
"Works only for qBittorrent.",
|
||||
],
|
||||
|
||||
},
|
||||
{
|
||||
name: "DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR",
|
||||
description: [
|
||||
@@ -30,11 +44,17 @@ const settings: EnvVarProps[] = [
|
||||
title: "Multiple patterns can be specified using incrementing numbers starting from 0.",
|
||||
content: `DOWNLOADCLEANER__UNLINKED_CATEGORIES__0=tv-sonarr
|
||||
DOWNLOADCLEANER__UNLINKED_CATEGORIES__1=radarr`
|
||||
}
|
||||
},
|
||||
],
|
||||
type: "text",
|
||||
defaultValue: "Empty",
|
||||
required: false,
|
||||
notes: [
|
||||
"The category name must match the category that was set in the *arr.",
|
||||
"For qBittorrent, the category name is the name of the download category.",
|
||||
"For Deluge, the category name is the name of the label.",
|
||||
"For Transmission, the category name is the last directory from the save location.",
|
||||
],
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ const settings: EnvVarProps[] = [
|
||||
type: "positive integer number",
|
||||
defaultValue: "0",
|
||||
required: false,
|
||||
examples: ["0", "3", "10"],
|
||||
notes: [
|
||||
"`0` means to never remove failed imports.",
|
||||
"If not set to `0`, the minimum value is `3`."
|
||||
]
|
||||
},
|
||||
|
||||
@@ -11,8 +11,8 @@ const settings: EnvVarProps[] = [
|
||||
type: "positive integer number",
|
||||
defaultValue: "0",
|
||||
required: false,
|
||||
examples: ["0", "3", "10"],
|
||||
notes: [
|
||||
"`0` means to never remove stalled downloads.",
|
||||
"If not set to 0, the minimum value is `3`."
|
||||
]
|
||||
},
|
||||
@@ -59,6 +59,7 @@ const settings: EnvVarProps[] = [
|
||||
defaultValue: "0",
|
||||
required: false,
|
||||
notes: [
|
||||
"`0` means to never remove downloads stuck while downloading metadata.",
|
||||
"If not set to `0`, the minimum value is `3`."
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user