Compare commits

...

12 Commits

Author SHA1 Message Date
Flaminel
c82b5e11b1 Add rate limiting for download removal (#141) 2025-05-11 13:27:51 +03:00
Flaminel
c36d9eb9cf fixed Transmission docs 2025-05-11 10:16:13 +03:00
Flaminel
2f21603e8e fixed docs urls 2025-05-10 14:28:26 +03:00
Flaminel
586f9964b5 Update docs (#138) 2025-05-10 14:28:40 +03:00
Flaminel
124670bb98 updated features 2025-05-09 14:39:39 +03:00
Flaminel
baf6a8c2f4 Add option to set failed import strikes per arr (#135) 2025-05-09 01:17:16 +03:00
Flaminel
cd345afc54 Fix logs when using qBit tag instead of category (#134) 2025-05-08 22:50:14 +03:00
Flaminel
246ec4d6eb Add option to set a tag instead of changing the category for unlinked downloads (#133) 2025-05-08 21:51:08 +03:00
Flaminel
569eeae181 Fix hardlinks on ARM64 (#130) 2025-05-07 21:44:49 +03:00
Flaminel
5a0ef56074 Remove empty clean categories list validation (#131) 2025-05-07 14:12:36 +03:00
Flaminel
09bd4321fb fixed readme icons 2025-05-06 19:59:00 +03:00
Flaminel
4939e37210 updated discord invite 2025-05-06 15:57:12 +03:00
68 changed files with 1233 additions and 376 deletions

View File

@@ -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

View File

@@ -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
[![Discord](https://img.shields.io/discord/1306721212587573389?color=7289DA&label=Discord&style=for-the-badge&logo=discord)](https://discord.gg/sWggpnmGNY)
[![Discord](https://img.shields.io/discord/1306721212587573389?color=7289DA&label=Discord&style=for-the-badge&logo=discord)](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.
@@ -10,13 +10,16 @@ Cleanuperr was created primarily to address malicious files, such as `*.lnk` or
> [!IMPORTANT]
> **Features:**
> - Strike system to mark stalled or downloads stuck in metadata downloading.
> - Strike system to mark bad downloads.
> - Remove and block downloads that reached a maximum number of strikes.
> - 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.
> - Clean up downloads that have been seeding for a certain amount of time.
> - Notify on strike or download removal.
> - Remove and block downloads that are **failing to be imported** by the arrs. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/queue-cleaner/import-failed)
> - Remove and block downloads that are **stalled** or in **metadata downloading** state. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/queue-cleaner/stalled)
> - Remove and block downloads that have a **low download speed** or **high estimated completion time**. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/queue-cleaner/slow)
> - Remove and block downloads blocked by qBittorrent or by Cleanuperr's **Content Blocker**. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/content-blocker/general)
> - Automatically trigger a search for downloads removed from the arrs.
> - Clean up downloads that have been **seeding** for a certain amount of time. [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/download-cleaner/seeding)
> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support). [configuration](https://flmorg.github.io/cleanuperr/docs/configuration/download-cleaner/hardlinks)
> - Notify on strike or download removal. [configuration](https://flmorg.github.io/cleanuperr/docs/category/notifications)
> - 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.
@@ -38,13 +41,13 @@ Cleanuperr supports both qBittorrent's built-in exclusion features and its own b
Docs can be found [here](https://flmorg.github.io/cleanuperr/).
# <img width="24px" src="./Logo/256.png" alt="Cleanuperr"> Cleanuperr <svg width="14px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M376.6 84.5c11.3-13.6 9.5-33.8-4.1-45.1s-33.8-9.5-45.1 4.1L192 206 56.6 43.5C45.3 29.9 25.1 28.1 11.5 39.4S-3.9 70.9 7.4 84.5L150.3 256 7.4 427.5c-11.3 13.6-9.5 33.8 4.1 45.1s33.8 9.5 45.1-4.1L192 306 327.4 468.5c11.3 13.6 31.5 15.4 45.1 4.1s15.4-31.5 4.1-45.1L233.7 256 376.6 84.5z"/></svg> Huntarr <img width="24px" src="https://github.com/plexguide/Huntarr.io/blob/main/frontend/static/logo/256.png?raw=true" alt Huntarr></img>
# <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) ![Huntarr](https://img.shields.io/github/stars/plexguide/Huntarr.io?style=social)</span>
<span style="font-size:24px"> ➡️ [**Huntarr**](https://github.com/plexguide/Huntarr.io) <span style="vertical-align: middle">![Huntarr](https://img.shields.io/github/stars/plexguide/Huntarr.io?style=social)</span></span>
# Credits
Special thanks for inspiration go to:

View File

@@ -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; }
}

View File

@@ -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");

View File

@@ -0,0 +1,12 @@
using Microsoft.Extensions.Configuration;
namespace Common.Configuration.General;
public sealed record SearchConfig
{
[ConfigurationKeyName("SEARCH_ENABLED")]
public bool SearchEnabled { get; init; } = true;
[ConfigurationKeyName("SEARCH_DELAY")]
public ushort SearchDelay { get; init; } = 30;
}

View File

@@ -13,6 +13,7 @@ public static class ConfigurationDI
public static IServiceCollection AddConfiguration(this IServiceCollection services, IConfiguration configuration) =>
services
.Configure<DryRunConfig>(configuration)
.Configure<SearchConfig>(configuration)
.Configure<QueueCleanerConfig>(configuration.GetSection(QueueCleanerConfig.SectionName))
.Configure<ContentBlockerConfig>(configuration.GetSection(ContentBlockerConfig.SectionName))
.Configure<DownloadCleanerConfig>(configuration.GetSection(DownloadCleanerConfig.SectionName))

View File

@@ -1,11 +1,14 @@
using System.Net;
using Common.Configuration.General;
using Common.Helpers;
using Domain.Models.Arr;
using Infrastructure.Services;
using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.DownloadRemover.Consumers;
using Infrastructure.Verticals.Notifications.Consumers;
using Infrastructure.Verticals.Notifications.Models;
using MassTransit;
using MassTransit.Configuration;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Extensions.Http;
@@ -27,6 +30,9 @@ public static class MainDI
.AddNotifications(configuration)
.AddMassTransit(config =>
{
config.AddConsumer<DownloadRemoverConsumer<SearchItem>>();
config.AddConsumer<DownloadRemoverConsumer<SonarrSearchItem>>();
config.AddConsumer<NotificationConsumer<FailedImportStrikeNotification>>();
config.AddConsumer<NotificationConsumer<StalledStrikeNotification>>();
config.AddConsumer<NotificationConsumer<SlowStrikeNotification>>();
@@ -36,6 +42,14 @@ public static class MainDI
config.UsingInMemory((context, cfg) =>
{
cfg.ReceiveEndpoint("download-remover-queue", e =>
{
e.ConfigureConsumer<DownloadRemoverConsumer<SearchItem>>(context);
e.ConfigureConsumer<DownloadRemoverConsumer<SonarrSearchItem>>(context);
e.ConcurrentMessageLimit = 1;
e.PrefetchCount = 1;
});
cfg.ReceiveEndpoint("notification-queue", e =>
{
e.ConfigureConsumer<NotificationConsumer<FailedImportStrikeNotification>>(context);

View File

@@ -11,6 +11,8 @@ using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.DownloadClient.Deluge;
using Infrastructure.Verticals.DownloadClient.QBittorrent;
using Infrastructure.Verticals.DownloadClient.Transmission;
using Infrastructure.Verticals.DownloadRemover;
using Infrastructure.Verticals.DownloadRemover.Interfaces;
using Infrastructure.Verticals.Files;
using Infrastructure.Verticals.ItemStriker;
using Infrastructure.Verticals.QueueCleaner;
@@ -26,9 +28,11 @@ public static class ServicesDI
.AddTransient<SonarrClient>()
.AddTransient<RadarrClient>()
.AddTransient<LidarrClient>()
.AddTransient<ArrClientFactory>()
.AddTransient<QueueCleaner>()
.AddTransient<ContentBlocker>()
.AddTransient<DownloadCleaner>()
.AddTransient<IQueueItemRemover, QueueItemRemover>()
.AddTransient<IFilenameEvaluator, FilenameEvaluator>()
.AddTransient<IHardLinkFileService, HardLinkFileService>()
.AddTransient<UnixHardLinkFileService>()

View File

@@ -11,6 +11,8 @@
"Path": ""
}
},
"SEARCH_ENABLED": true,
"SEARCH_DELAY": 5,
"Triggers": {
"QueueCleaner": "0/10 * * * * ?",
"ContentBlocker": "0/10 * * * * ?",
@@ -57,6 +59,7 @@
}
],
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
"UNLINKED_USE_TAG": false,
"UNLINKED_IGNORED_ROOT_DIR": "",
"UNLINKED_CATEGORIES": [
"tv-sonarr",
@@ -84,6 +87,7 @@
},
"Sonarr": {
"Enabled": true,
"IMPORT_FAILED_MAX_STRIKES": -1,
"SearchType": "Episode",
"Block": {
"Type": "blacklist",
@@ -98,6 +102,7 @@
},
"Radarr": {
"Enabled": true,
"IMPORT_FAILED_MAX_STRIKES": -1,
"Block": {
"Type": "blacklist",
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"
@@ -111,6 +116,7 @@
},
"Lidarr": {
"Enabled": true,
"IMPORT_FAILED_MAX_STRIKES": -1,
"Block": {
"Type": "blacklist",
"Path": "https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist"

View File

@@ -11,6 +11,8 @@
"Path": ""
}
},
"SEARCH_ENABLED": true,
"SEARCH_DELAY": 30,
"Triggers": {
"QueueCleaner": "0 0/5 * * * ?",
"ContentBlocker": "0 0/5 * * * ?",
@@ -47,6 +49,7 @@
"DELETE_PRIVATE": false,
"CATEGORIES": [],
"UNLINKED_TARGET_CATEGORY": "cleanuperr-unlinked",
"UNLINKED_USE_TAG": false,
"UNLINKED_IGNORED_ROOT_DIR": "",
"UNLINKED_CATEGORIES": [],
"IGNORED_DOWNLOADS_PATH": ""
@@ -71,6 +74,7 @@
},
"Sonarr": {
"Enabled": false,
"IMPORT_FAILED_MAX_STRIKES": -1,
"SearchType": "Episode",
"Block": {
"Type": "blacklist",
@@ -85,6 +89,7 @@
},
"Radarr": {
"Enabled": false,
"IMPORT_FAILED_MAX_STRIKES": -1,
"Block": {
"Type": "blacklist",
"Path": ""
@@ -98,6 +103,7 @@
},
"Lidarr": {
"Enabled": false,
"IMPORT_FAILED_MAX_STRIKES": -1,
"Block": {
"Type": "blacklist",
"Path": ""

View File

@@ -1,5 +1,6 @@
using Domain.Models.Deluge.Response;
using Infrastructure.Helpers;
using Infrastructure.Services;
namespace Infrastructure.Extensions;

View File

@@ -1,4 +1,5 @@
using Infrastructure.Helpers;
using Infrastructure.Services;
using QBittorrent.Client;
namespace Infrastructure.Extensions;

View File

@@ -1,4 +1,5 @@
using Infrastructure.Helpers;
using Infrastructure.Services;
using Transmission.API.RPC.Entity;
namespace Infrastructure.Extensions;

View File

@@ -13,4 +13,6 @@ public static class CacheKeys
public static string StrikeItem(string hash, StrikeType strikeType) => $"item_{hash}_{strikeType.ToString()}";
public static string IgnoredDownloads(string name) => $"{name}_ignored";
public static string DownloadMarkedForRemoval(string hash, Uri url) => $"remove_{hash.ToLowerInvariant()}_{url}";
}

View File

@@ -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>

View File

@@ -1,6 +1,6 @@
using System.Text.RegularExpressions;
namespace Infrastructure.Helpers;
namespace Infrastructure.Services;
public static class UriService
{

View File

@@ -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
);
}
@@ -149,7 +157,7 @@ public abstract class ArrClient : IArrClient
}
}
public abstract Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
public abstract Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
public virtual bool IsRecordValid(QueueRecord record)
{

View File

@@ -0,0 +1,31 @@
using Domain.Enums;
using Infrastructure.Verticals.Arr.Interfaces;
namespace Infrastructure.Verticals.Arr;
public sealed class ArrClientFactory
{
private readonly ISonarrClient _sonarrClient;
private readonly IRadarrClient _radarrClient;
private readonly ILidarrClient _lidarrClient;
public ArrClientFactory(
SonarrClient sonarrClient,
RadarrClient radarrClient,
LidarrClient lidarrClient
)
{
_sonarrClient = sonarrClient;
_radarrClient = radarrClient;
_lidarrClient = lidarrClient;
}
public IArrClient GetClient(InstanceType type) =>
type switch
{
InstanceType.Sonarr => _sonarrClient,
InstanceType.Radarr => _radarrClient,
InstanceType.Lidarr => _lidarrClient,
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
}

View File

@@ -9,11 +9,11 @@ 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);
Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
bool IsRecordValid(QueueRecord record);
}

View File

@@ -50,7 +50,7 @@ public class LidarrClient : ArrClient, ILidarrClient
return query;
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
public override async Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
{
if (items?.Count is null or 0)
{

View File

@@ -50,7 +50,7 @@ public class RadarrClient : ArrClient, IRadarrClient
return query;
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
public override async Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
{
if (items?.Count is null or 0)
{

View File

@@ -51,7 +51,7 @@ public class SonarrClient : ArrClient, ISonarrClient
return query;
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
public override async Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
{
if (items?.Count is null or 0)
{

View File

@@ -6,16 +6,20 @@ using Common.Configuration.DownloadClient;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Helpers;
using Infrastructure.Providers;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.Arr.Interfaces;
using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.DownloadRemover.Models;
using Infrastructure.Verticals.Jobs;
using Infrastructure.Verticals.Notifications;
using MassTransit;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog.Context;
using LogContext = Serilog.Context.LogContext;
namespace Infrastructure.Verticals.ContentBlocker;
@@ -32,9 +36,9 @@ public sealed class ContentBlocker : GenericHandler
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig,
IOptions<LidarrConfig> lidarrConfig,
SonarrClient sonarrClient,
RadarrClient radarrClient,
LidarrClient lidarrClient,
IMemoryCache cache,
IBus messageBus,
ArrClientFactory arrClientFactory,
ArrQueueIterator arrArrQueueIterator,
BlocklistProvider blocklistProvider,
DownloadServiceFactory downloadServiceFactory,
@@ -43,8 +47,7 @@ public sealed class ContentBlocker : GenericHandler
) : base(
logger, downloadClientConfig,
sonarrConfig, radarrConfig, lidarrConfig,
sonarrClient, radarrClient, lidarrClient,
arrArrQueueIterator, downloadServiceFactory,
cache, messageBus, arrClientFactory, arrArrQueueIterator, downloadServiceFactory,
notifier
)
{
@@ -75,21 +78,16 @@ 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();
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
HashSet<SearchItem> itemsToBeRefreshed = [];
IArrClient arrClient = GetClient(instanceType);
IArrClient arrClient = _arrClientFactory.GetClient(instanceType);
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
// push to context
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), instance.Url);
ContextProvider.Set(nameof(InstanceType), instanceType);
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
{
@@ -117,9 +115,14 @@ public sealed class ContentBlocker : GenericHandler
_logger.LogInformation("skip | {title} | ignored", record.Title);
continue;
}
string downloadRemovalKey = CacheKeys.DownloadMarkedForRemoval(record.DownloadId, instance.Url);
// push record to context
ContextProvider.Set(nameof(QueueRecord), record);
if (_cache.TryGetValue(downloadRemovalKey, out bool _))
{
_logger.LogDebug("skip | already marked for removal | {title}", record.Title);
continue;
}
_logger.LogDebug("searching unwanted files for {title}", record.Title);
@@ -133,8 +136,6 @@ public sealed class ContentBlocker : GenericHandler
_logger.LogDebug("all files are marked as unwanted | {hash}", record.Title);
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
bool removeFromClient = true;
if (result.IsPrivate && !_config.DeletePrivate)
@@ -142,11 +143,16 @@ public sealed class ContentBlocker : GenericHandler
removeFromClient = false;
}
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient, DeleteReason.AllFilesBlocked);
await _notifier.NotifyQueueItemDeleted(removeFromClient, DeleteReason.AllFilesBlocked);
await PublishQueueItemRemoveRequest(
downloadRemovalKey,
instanceType,
instance,
record,
group.Count() > 1,
removeFromClient,
DeleteReason.AllFilesBlocked
);
}
});
await arrClient.RefreshItemsAsync(instance, itemsToBeRefreshed);
}
}

View File

@@ -9,9 +9,11 @@ using Infrastructure.Verticals.Arr.Interfaces;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.Jobs;
using Infrastructure.Verticals.Notifications;
using MassTransit;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog.Context;
using LogContext = Serilog.Context.LogContext;
namespace Infrastructure.Verticals.DownloadCleaner;
@@ -30,9 +32,9 @@ public sealed class DownloadCleaner : GenericHandler
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig,
IOptions<LidarrConfig> lidarrConfig,
SonarrClient sonarrClient,
RadarrClient radarrClient,
LidarrClient lidarrClient,
IMemoryCache cache,
IBus messageBus,
ArrClientFactory arrClientFactory,
ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory,
INotificationPublisher notifier,
@@ -40,8 +42,7 @@ public sealed class DownloadCleaner : GenericHandler
) : base(
logger, downloadClientConfig,
sonarrConfig, radarrConfig, lidarrConfig,
sonarrClient, radarrClient, lidarrClient,
arrArrQueueIterator, downloadServiceFactory,
cache, messageBus, arrClientFactory, arrArrQueueIterator, downloadServiceFactory,
notifier
)
{
@@ -58,9 +59,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 +72,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,24 +105,34 @@ 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());
IArrClient arrClient = GetClient(instanceType);
IArrClient arrClient = _arrClientFactory.GetClient(instanceType);
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
{

View File

@@ -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);
}

View File

@@ -0,0 +1,39 @@
using Domain.Models.Arr;
using Infrastructure.Verticals.DownloadRemover.Interfaces;
using Infrastructure.Verticals.DownloadRemover.Models;
using MassTransit;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Verticals.DownloadRemover.Consumers;
public class DownloadRemoverConsumer<T> : IConsumer<QueueItemRemoveRequest<T>>
where T : SearchItem
{
private readonly ILogger<DownloadRemoverConsumer<T>> _logger;
private readonly IQueueItemRemover _queueItemRemover;
public DownloadRemoverConsumer(
ILogger<DownloadRemoverConsumer<T>> logger,
IQueueItemRemover queueItemRemover
)
{
_logger = logger;
_queueItemRemover = queueItemRemover;
}
public async Task Consume(ConsumeContext<QueueItemRemoveRequest<T>> context)
{
try
{
await _queueItemRemover.RemoveQueueItemAsync(context.Message);
}
catch (Exception exception)
{
_logger.LogError(exception,
"failed to remove queue item| {title} | {url}",
context.Message.Record.Title,
context.Message.Instance.Url
);
}
}
}

View File

@@ -0,0 +1,9 @@
using Domain.Models.Arr;
using Infrastructure.Verticals.DownloadRemover.Models;
namespace Infrastructure.Verticals.DownloadRemover.Interfaces;
public interface IQueueItemRemover
{
Task RemoveQueueItemAsync<T>(QueueItemRemoveRequest<T> request) where T : SearchItem;
}

View File

@@ -0,0 +1,22 @@
using Common.Configuration.Arr;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
namespace Infrastructure.Verticals.DownloadRemover.Models;
public sealed record QueueItemRemoveRequest<T>
where T : SearchItem
{
public required InstanceType InstanceType { get; init; }
public required ArrInstance Instance { get; init; }
public required T SearchItem { get; init; }
public required QueueRecord Record { get; init; }
public required bool RemoveFromClient { get; init; }
public required DeleteReason DeleteReason { get; init; }
}

View File

@@ -0,0 +1,66 @@
using Common.Configuration.Arr;
using Common.Configuration.General;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Helpers;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.DownloadRemover.Interfaces;
using Infrastructure.Verticals.DownloadRemover.Models;
using Infrastructure.Verticals.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.DownloadRemover;
public sealed class QueueItemRemover : IQueueItemRemover
{
private readonly SearchConfig _searchConfig;
private readonly IMemoryCache _cache;
private readonly ArrClientFactory _arrClientFactory;
private readonly INotificationPublisher _notifier;
public QueueItemRemover(
IOptions<SearchConfig> searchConfig,
IMemoryCache cache,
ArrClientFactory arrClientFactory,
INotificationPublisher notifier
)
{
_searchConfig = searchConfig.Value;
_cache = cache;
_arrClientFactory = arrClientFactory;
_notifier = notifier;
}
public async Task RemoveQueueItemAsync<T>(QueueItemRemoveRequest<T> request)
where T : SearchItem
{
try
{
var arrClient = _arrClientFactory.GetClient(request.InstanceType);
await arrClient.DeleteQueueItemAsync(request.Instance, request.Record, request.RemoveFromClient, request.DeleteReason);
// push to context
ContextProvider.Set(nameof(QueueRecord), request.Record);
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), request.Instance.Url);
ContextProvider.Set(nameof(InstanceType), request.InstanceType);
await _notifier.NotifyQueueItemDeleted(request.RemoveFromClient, request.DeleteReason);
if (!_searchConfig.SearchEnabled)
{
return;
}
await arrClient.SearchItemsAsync(request.Instance, [request.SearchItem]);
// prevent tracker spamming
await Task.Delay(TimeSpan.FromSeconds(_searchConfig.SearchDelay));
}
finally
{
_cache.Remove(CacheKeys.DownloadMarkedForRemoval(request.Record.DownloadId, request.Instance.Url));
}
}
}

View File

@@ -4,9 +4,11 @@ using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.Arr.Interfaces;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.DownloadRemover.Models;
using Infrastructure.Verticals.Notifications;
using MassTransit;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -19,9 +21,9 @@ public abstract class GenericHandler : IHandler, IDisposable
protected readonly SonarrConfig _sonarrConfig;
protected readonly RadarrConfig _radarrConfig;
protected readonly LidarrConfig _lidarrConfig;
protected readonly ISonarrClient _sonarrClient;
protected readonly IRadarrClient _radarrClient;
protected readonly ILidarrClient _lidarrClient;
protected readonly IMemoryCache _cache;
protected readonly IBus _messageBus;
protected readonly ArrClientFactory _arrClientFactory;
protected readonly ArrQueueIterator _arrArrQueueIterator;
protected readonly IDownloadService _downloadService;
protected readonly INotificationPublisher _notifier;
@@ -32,9 +34,9 @@ public abstract class GenericHandler : IHandler, IDisposable
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig,
IOptions<LidarrConfig> lidarrConfig,
ISonarrClient sonarrClient,
IRadarrClient radarrClient,
ILidarrClient lidarrClient,
IMemoryCache cache,
IBus messageBus,
ArrClientFactory arrClientFactory,
ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory,
INotificationPublisher notifier
@@ -45,9 +47,9 @@ public abstract class GenericHandler : IHandler, IDisposable
_sonarrConfig = sonarrConfig.Value;
_radarrConfig = radarrConfig.Value;
_lidarrConfig = lidarrConfig.Value;
_sonarrClient = sonarrClient;
_radarrClient = radarrClient;
_lidarrClient = lidarrClient;
_cache = cache;
_messageBus = messageBus;
_arrClientFactory = arrClientFactory;
_arrArrQueueIterator = arrArrQueueIterator;
_downloadService = downloadServiceFactory.CreateDownloadClient();
_notifier = notifier;
@@ -67,7 +69,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 +82,7 @@ public abstract class GenericHandler : IHandler, IDisposable
{
try
{
await ProcessInstanceAsync(arrInstance, instanceType);
await ProcessInstanceAsync(arrInstance, instanceType, config);
}
catch (Exception exception)
{
@@ -93,16 +95,50 @@ public abstract class GenericHandler : IHandler, IDisposable
}
}
}
protected IArrClient GetClient(InstanceType type) =>
type switch
{
InstanceType.Sonarr => _sonarrClient,
InstanceType.Radarr => _radarrClient,
InstanceType.Lidarr => _lidarrClient,
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
};
protected async Task PublishQueueItemRemoveRequest(
string downloadRemovalKey,
InstanceType instanceType,
ArrInstance instance,
QueueRecord record,
bool isPack,
bool removeFromClient,
DeleteReason deleteReason
)
{
if (instanceType is InstanceType.Sonarr)
{
QueueItemRemoveRequest<SonarrSearchItem> removeRequest = new()
{
InstanceType = instanceType,
Instance = instance,
Record = record,
SearchItem = (SonarrSearchItem)GetRecordSearchItem(instanceType, record, isPack),
RemoveFromClient = removeFromClient,
DeleteReason = deleteReason
};
await _messageBus.Publish(removeRequest);
}
else
{
QueueItemRemoveRequest<SearchItem> removeRequest = new()
{
InstanceType = instanceType,
Instance = instance,
Record = record,
SearchItem = GetRecordSearchItem(instanceType, record, isPack),
RemoveFromClient = removeFromClient,
DeleteReason = deleteReason
};
await _messageBus.Publish(removeRequest);
}
_cache.Set(downloadRemovalKey, true);
_logger.LogInformation("item marked for removal | {title} | {url}", record.Title, instance.Url);
}
protected SearchItem GetRecordSearchItem(InstanceType type, QueueRecord record, bool isPack = false)
{
return type switch

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -4,22 +4,27 @@ using Common.Configuration.QueueCleaner;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Helpers;
using Infrastructure.Providers;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.Arr.Interfaces;
using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.DownloadRemover.Models;
using Infrastructure.Verticals.Jobs;
using Infrastructure.Verticals.Notifications;
using MassTransit;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog.Context;
using LogContext = Serilog.Context.LogContext;
namespace Infrastructure.Verticals.QueueCleaner;
public sealed class QueueCleaner : GenericHandler
{
private readonly QueueCleanerConfig _config;
private readonly IMemoryCache _cache;
private readonly IgnoredDownloadsProvider<QueueCleanerConfig> _ignoredDownloadsProvider;
public QueueCleaner(
@@ -29,9 +34,9 @@ public sealed class QueueCleaner : GenericHandler
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig,
IOptions<LidarrConfig> lidarrConfig,
SonarrClient sonarrClient,
RadarrClient radarrClient,
LidarrClient lidarrClient,
IMemoryCache cache,
IBus messageBus,
ArrClientFactory arrClientFactory,
ArrQueueIterator arrArrQueueIterator,
DownloadServiceFactory downloadServiceFactory,
INotificationPublisher notifier,
@@ -39,24 +44,23 @@ public sealed class QueueCleaner : GenericHandler
) : base(
logger, downloadClientConfig,
sonarrConfig, radarrConfig, lidarrConfig,
sonarrClient, radarrClient, lidarrClient,
arrArrQueueIterator, downloadServiceFactory,
cache, messageBus, arrClientFactory, arrArrQueueIterator, downloadServiceFactory,
notifier
)
{
_config = config.Value;
_config.Validate();
_cache = cache;
_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();
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
HashSet<SearchItem> itemsToBeRefreshed = [];
IArrClient arrClient = GetClient(instanceType);
IArrClient arrClient = _arrClientFactory.GetClient(instanceType);
// push to context
ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), instance.Url);
@@ -90,6 +94,14 @@ public sealed class QueueCleaner : GenericHandler
continue;
}
string downloadRemovalKey = CacheKeys.DownloadMarkedForRemoval(record.DownloadId, instance.Url);
if (_cache.TryGetValue(downloadRemovalKey, out bool _))
{
_logger.LogDebug("skip | already marked for removal | {title}", record.Title);
continue;
}
// push record to context
ContextProvider.Set(nameof(QueueRecord), record);
@@ -108,7 +120,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)
@@ -116,8 +128,6 @@ public sealed class QueueCleaner : GenericHandler
_logger.LogInformation("skip | {title}", record.Title);
continue;
}
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
bool removeFromClient = true;
@@ -140,11 +150,16 @@ public sealed class QueueCleaner : GenericHandler
}
}
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient, deleteReason);
await _notifier.NotifyQueueItemDeleted(removeFromClient, deleteReason);
await PublishQueueItemRemoveRequest(
downloadRemovalKey,
instanceType,
instance,
record,
group.Count() > 1,
removeFromClient,
deleteReason
);
}
});
await arrClient.RefreshItemsAsync(instance, itemsToBeRefreshed);
}
}

View File

@@ -186,6 +186,9 @@ services:
- HTTP_MAX_RETRIES=0
- HTTP_TIMEOUT=20
- SEARCH_ENABLED=true
- SEARCH_DELAY=5
- TRIGGERS__QUEUECLEANER=0/30 * * * * ?
- TRIGGERS__CONTENTBLOCKER=0/30 * * * * ?
- TRIGGERS__DOWNLOADCLEANER=0/30 * * * * ?
@@ -221,15 +224,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 +255,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 +263,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

View File

@@ -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>

21
docs/docs/2_features.mdx Normal file
View File

@@ -0,0 +1,21 @@
---
sidebar_position: 2
---
import Link from '@docusaurus/Link';
# Features
<div style={{ fontSize: '1.2rem' }}>
- Strike system to mark bad downloads.
- Remove and block downloads that reached a maximum number of strikes.
- Remove and block downloads that are **failing to be imported** by the arrs. <Link to="/docs/configuration/queue-cleaner/import-failed">[configuration]</Link>
- Remove and block downloads that are **stalled** or in **metadata downloading** state. <Link to="/docs/configuration/queue-cleaner/stalled">[configuration]</Link>
- Remove and block downloads that have a **low download speed** or **high estimated completion time**. <Link to="/docs/configuration/queue-cleaner/slow">[configuration]</Link>
- Remove and block downloads blocked by qBittorrent or by Cleanuperr's **Content Blocker**. <Link to="/docs/configuration/content-blocker/general">[configuration]</Link>
- Automatically trigger a search for downloads removed from the arrs.
- Clean up downloads that have been **seeding** for a certain amount of time. <Link to="/docs/configuration/download-cleaner/seeding">[configuration]</Link>
- Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support). <Link to="/docs/configuration/download-cleaner/hardlinks">[configuration]</Link>
- Notify on strike or download removal. <Link to="/docs/category/notifications">[configuration]</Link>
- Ignore certain torrent hashes, categories, tags or trackers from being processed by Cleanuperr.
</div>

View File

@@ -1,10 +1,10 @@
---
sidebar_position: 1
sidebar_position: 2
---
import GeneralSettings from '@site/src/components/configuration/GeneralSettings';
# General Settings
# General settings
These are the general configuration settings that apply to the entire application.

View File

@@ -0,0 +1,11 @@
---
sidebar_position: 3
---
import SearchSettings from '@site/src/components/configuration/SearchSettings';
# Search settings
These are the search configuration settings when searching for replacements.
<SearchSettings/>

View File

@@ -1,6 +1,6 @@
{
"label": "Arrs settings",
"position": 6,
"position": 7,
"link": {
"type": "generated-index",
"description": "Servarr settings."

View File

@@ -3,6 +3,8 @@ sidebar_position: 1
---
import ContentBlockerGeneralSettings from '@site/src/components/configuration/content-blocker/ContentBlockerGeneralSettings';
import { Important } from '@site/src/components/Admonition';
import Link from '@docusaurus/Link';
# General Settings
@@ -16,4 +18,8 @@ These environment variables are needed to enable the Content Blocker functionali
- [LIDARR__BLOCK__TYPE](/docs/configuration/arrs/lidarr?LIDARR__BLOCK__TYPE) (if Lidarr is enabled)
- [LIDARR__BLOCK__PATH](/docs/configuration/arrs/lidarr?LIDARR__BLOCK__PATH) (if Lidarr is enabled)
<Important>
These settings need a <Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">download client</Link> to be configured.
</Important>
<ContentBlockerGeneralSettings/>

View File

@@ -1,6 +1,6 @@
{
"label": "Content Blocker",
"position": 2,
"position": 4,
"link": {
"type": "generated-index",
"description": "Settings for the Content Blocker functionality."

View File

@@ -3,9 +3,10 @@ sidebar_position: 2
---
import DownloadCleanerCleanupSettings from '@site/src/components/configuration/download-cleaner/DownloadCleanerCleanupSettings';
import { Note } from '@site/src/components/Admonition';
import { Note, Important } from '@site/src/components/Admonition';
import Link from '@docusaurus/Link';
# Cleanup Settings
# Seeding settings
These settings control how the Download Cleaner handles different categories of downloads that need to be removed.
@@ -23,4 +24,8 @@ These settings control how the Download Cleaner handles different categories of
```
</Note>
<Important>
These settings need a <Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">download client</Link> to be configured.
</Important>
<DownloadCleanerCleanupSettings/>

View File

@@ -4,12 +4,17 @@ sidebar_position: 3
import DownloadCleanerHardlinksSettings from '@site/src/components/configuration/download-cleaner/DownloadCleanerHardlinksSettings';
import { Important, Warning } from '@site/src/components/Admonition';
import Link from '@docusaurus/Link';
# Hardlinks Settings
These settings control how the Download Cleaner handles downloads with no hardlinks remaining (they are not available in the arrs anymore).
The Download Cleaner will change the category of a download that has no hardlinks and the new category can be cleaned based on the rules configured [here](/docs/configuration/download-cleaner/categories).
The Download Cleaner will change the category of a download that has no hardlinks and the new category can be cleaned based on the rules configured [here](/docs/configuration/download-cleaner/seeding).
<Important>
These settings need a <Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">download client</Link> to be configured.
</Important>
<Important>
If you are using Docker, make sure to mount the downloads directory the same way it is mounted for the download client.

View File

@@ -1,6 +1,6 @@
{
"label": "Download Cleaner",
"position": 3,
"position": 5,
"link": {
"type": "generated-index",
"description": "Configure the Download Cleaner to automatically clean up downloads that have been seeding for a certain amount of time."

View File

@@ -4,7 +4,7 @@ sidebar_position: 1
import DownloadClientSettings from '@site/src/components/configuration/download-client/DownloadClientSettings';
# Download Client Settings
# General settings
These settings control how Cleanuperr interacts with your download client.

View File

@@ -0,0 +1,7 @@
import QBittorrentSettings from '@site/src/components/configuration/download-client/QBittorrentSettings';
# qBittorrent settings
Settings used to access your qBittorrent instance.
<QBittorrentSettings/>

View File

@@ -0,0 +1,7 @@
import DelugeSettings from '@site/src/components/configuration/download-client/DelugeSettings';
# Deluge settings
Settings used to access your Deluge instance.
<DelugeSettings/>

View File

@@ -0,0 +1,7 @@
import TransmissionSettings from '@site/src/components/configuration/download-client/TransmissionSettings';
# Transmission settings
Settings used to access your Transmission instance.
<TransmissionSettings/>

View File

@@ -1,6 +1,6 @@
{
"label": "Download Client",
"position": 4,
"position": 6,
"link": {
"type": "generated-index",
"description": "Configure the download client settings for Cleanuperr."

View File

@@ -1,157 +1,394 @@
import { Note } from '@site/src/components/Admonition';
import CodeBlock from '@theme/CodeBlock';
import CodeBlockContainer from '@theme/CodeBlock/Container';
import Link from '@docusaurus/Link';
import styles from './examples.module.css';
# Docker compose
<Note>
**This example contains all settings and should be modified to fit your needs.**
This example contains all settings and should be modified to fit your needs.
Remove the variables that you do not need.
</Note>
```
services:
cleanuperr:
image: ghcr.io/flmorg/cleanuperr:latest
restart: unless-stopped
volumes:
- ./cleanuperr/logs:/var/logs
- ./cleanuperr/ignored.txt:/ignored.txt
environment:
# general settings
- TZ=America/New_York
- DRY_RUN=false
- HTTP_MAX_RETRIES=0
- HTTP_TIMEOUT=100
<Note>
Click on an environment variable's name to go to its documentation.
</Note>
# logging
- LOGGING__LOGLEVEL=Information
- LOGGING__FILE__ENABLED=false
- LOGGING__FILE__PATH=/var/logs/
- LOGGING__ENHANCED=true
# job triggers
- TRIGGERS__QUEUECLEANER=0 0/5 * * * ?
- TRIGGERS__CONTENTBLOCKER=0 0/5 * * * ?
- TRIGGERS__DOWNLOADCLEANER=0 0 * * * ?
# queue cleaner
- QUEUECLEANER__ENABLED=true
- QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- QUEUECLEANER__RUNSEQUENTIALLY=true
# failed imports
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
- QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=title mismatch
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required
# stalled downloads
- QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS=false
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
# slow downloads
- QUEUECLEANER__SLOW_MAX_STRIKES=5
- QUEUECLEANER__SLOW_RESET_STRIKES_ON_PROGRESS=true
- QUEUECLEANER__SLOW_IGNORE_PRIVATE=false
- QUEUECLEANER__SLOW_DELETE_PRIVATE=false
- QUEUECLEANER__SLOW_MIN_SPEED=1MB
- QUEUECLEANER__SLOW_MAX_TIME=20
- QUEUECLEANER__SLOW_IGNORE_ABOVE_SIZE=60GB
# content blocker
- CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- CONTENTBLOCKER__IGNORE_PRIVATE=false
- CONTENTBLOCKER__DELETE_PRIVATE=false
# download cleaner
- DOWNLOADCLEANER__ENABLED=true
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
- DOWNLOADCLEANER__DELETE_PRIVATE=false
# categories to seed until max ratio or min seed time has been reached
- 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=240
- DOWNLOADCLEANER__CATEGORIES__1__NAME=radarr
- DOWNLOADCLEANER__CATEGORIES__1__MAX_RATIO=-1
- DOWNLOADCLEANER__CATEGORIES__1__MIN_SEED_TIME=0
- DOWNLOADCLEANER__CATEGORIES__1__MAX_SEED_TIME=240
# remove downloads with no hardlinks
- DOWNLOADCLEANER__CATEGORIES__2__NAME=cleanuperr-unlinked
- DOWNLOADCLEANER__CATEGORIES__2__MAX_RATIO=-1
- DOWNLOADCLEANER__CATEGORIES__2__MIN_SEED_TIME=0
- DOWNLOADCLEANER__CATEGORIES__2__MAX_SEED_TIME=0
# change category for downloads with no hardlinks
- DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY=cleanuperr-unlinked
- DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR=/downloads
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__0=tv-sonarr
- DOWNLOADCLEANER__UNLINKED_CATEGORIES__1=radarr
- DOWNLOAD_CLIENT=none
# OR
# - DOWNLOAD_CLIENT=disabled
# OR
# - DOWNLOAD_CLIENT=qBittorrent
# - QBITTORRENT__URL=http://localhost:8080
# - QBITTORRENT__URL_BASE=myCustomPath
# - QBITTORRENT__USERNAME=user
# - QBITTORRENT__PASSWORD=pass
# OR
# - DOWNLOAD_CLIENT=deluge
# - DELUGE__URL_BASE=myCustomPath
# - DELUGE__URL=http://localhost:8112
# - DELUGE__PASSWORD=testing
# OR
# - DOWNLOAD_CLIENT=transmission
# - TRANSMISSION__URL=http://localhost:9091
# - TRANSMISSION__URL_BASE=myCustomPath
# - TRANSMISSION__USERNAME=test
# - TRANSMISSION__PASSWORD=testing
- SONARR__ENABLED=true
- SONARR__SEARCHTYPE=Episode
- SONARR__BLOCK__TYPE=blacklist
- SONARR__BLOCK__PATH=https://example.com/path/to/file.txt
- SONARR__INSTANCES__0__URL=http://localhost:8989
- SONARR__INSTANCES__0__APIKEY=secret1
- SONARR__INSTANCES__1__URL=http://localhost:8990
- SONARR__INSTANCES__1__APIKEY=secret2
- RADARR__ENABLED=true
- RADARR__BLOCK__TYPE=blacklist
- RADARR__BLOCK__PATH=https://example.com/path/to/file.txt
- RADARR__INSTANCES__0__URL=http://localhost:7878
- RADARR__INSTANCES__0__APIKEY=secret3
- RADARR__INSTANCES__1__URL=http://localhost:7879
- RADARR__INSTANCES__1__APIKEY=secret4
- LIDARR__ENABLED=true
- LIDARR__BLOCK__TYPE=blacklist
- LIDARR__BLOCK__PATH=https://example.com/path/to/file.txt
- LIDARR__INSTANCES__0__URL=http://radarr:8686
- LIDARR__INSTANCES__0__APIKEY=secret5
- LIDARR__INSTANCES__1__URL=http://radarr:8687
- LIDARR__INSTANCES__1__APIKEY=secret6
- NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true
- NOTIFIARR__ON_STALLED_STRIKE=true
- NOTIFIARR__ON_SLOW_STRIKE=true
- NOTIFIARR__ON_QUEUE_ITEM_DELETED=true
- NOTIFIARR__ON_DOWNLOAD_CLEANED=true
- NOTIFIARR__ON_CATEGORY_CHANGED=true
- NOTIFIARR__API_KEY=notifiarr_secret
- NOTIFIARR__CHANNEL_ID=discord_channel_id
- APPRISE__ON_IMPORT_FAILED_STRIKE=true
- APPRISE__ON_STALLED_STRIKE=true
- APPRISE__ON_SLOW_STRIKE=true
- APPRISE__ON_QUEUE_ITEM_DELETED=true
- APPRISE__ON_DOWNLOAD_CLEANED=true
- NOTIFIARR__ON_CATEGORY_CHANGED=true
- APPRISE__URL=http://apprise:8000
- APPRISE__KEY=myConfigKey
```
<CodeBlock language="text" title="Environment Variables" wrap={true}>
<div style={{ whiteSpace: 'pre' }}>
{`services:
cleanuperr:
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:
- `}
<Link to="/docs/configuration/general?TZ">TZ</Link>
{`=America/New_York
- `}
<Link to="/docs/configuration/general?DRY_RUN">DRY_RUN</Link>
{`=false
- `}
<Link to="/docs/configuration/general?HTTP_MAX_RETRIES">HTTP_MAX_RETRIES</Link>
{`=0
- `}
<Link to="/docs/configuration/general?HTTP_TIMEOUT">HTTP_TIMEOUT</Link>
{`=100
- `}
<Link to="/docs/configuration/general?HTTP_VALIDATE_CERT">HTTP_VALIDATE_CERT</Link>
{`=Enabled
- `}
<Link to="/docs/configuration/general?LOGGING__LOGLEVEL">LOGGING__LOGLEVEL</Link>
{`=Information
- `}
<Link to="/docs/configuration/general?LOGGING__FILE__ENABLED">LOGGING__FILE__ENABLED</Link>
{`=false
- `}
<Link to="/docs/configuration/general?LOGGING__FILE__PATH">LOGGING__FILE__PATH</Link>
{`=/var/logs/
- `}
<Link to="/docs/configuration/general?LOGGING__ENHANCED">LOGGING__ENHANCED</Link>
{`=true
- `}
<Link to="/docs/configuration/search?SEARCH_ENABLED">SEARCH_ENABLED</Link>
{`=true
- `}
<Link to="/docs/configuration/search?SEARCH_DELAY">SEARCH_DELAY</Link>
{`=30
- `}
<Link to="/docs/configuration/queue-cleaner/general?TRIGGERS__QUEUECLEANER">TRIGGERS__QUEUECLEANER</Link>
{`=0 0/5 * * * ?
- `}
<Link to="/docs/configuration/queue-cleaner/general?QUEUECLEANER__ENABLED">QUEUECLEANER__ENABLED</Link>
{`=true
- `}
<Link to="/docs/configuration/queue-cleaner/general?QUEUECLEANER__IGNORED_DOWNLOADS_PATH">QUEUECLEANER__IGNORED_DOWNLOADS_PATH</Link>
{`=/ignored.txt
- `}
<Link to="/docs/configuration/queue-cleaner/general?QUEUECLEANER__RUNSEQUENTIALLY">QUEUECLEANER__RUNSEQUENTIALLY</Link>
{`=true
- `}
<Link to="/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES">QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES</Link>
{`=5
- `}
<Link to="/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE">QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE">QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0">QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0</Link>
{`=title mismatch
- `}
<Link to="/docs/configuration/queue-cleaner/import-failed?QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1">QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1</Link>
{`=manual import required
- `}
<Link to="/docs/configuration/queue-cleaner/stalled?QUEUECLEANER__STALLED_MAX_STRIKES">QUEUECLEANER__STALLED_MAX_STRIKES</Link>
{`=5
- `}
<Link to="/docs/configuration/queue-cleaner/stalled?QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS">QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS</Link>
{`=true
- `}
<Link to="/docs/configuration/queue-cleaner/stalled?QUEUECLEANER__STALLED_IGNORE_PRIVATE">QUEUECLEANER__STALLED_IGNORE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/queue-cleaner/stalled?QUEUECLEANER__STALLED_DELETE_PRIVATE">QUEUECLEANER__STALLED_DELETE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/queue-cleaner/slow?QUEUECLEANER__SLOW_MAX_STRIKES">QUEUECLEANER__SLOW_MAX_STRIKES</Link>
{`=5
- `}
<Link to="/docs/configuration/queue-cleaner/slow?QUEUECLEANER__SLOW_RESET_STRIKES_ON_PROGRESS">QUEUECLEANER__SLOW_RESET_STRIKES_ON_PROGRESS</Link>
{`=true
- `}
<Link to="/docs/configuration/queue-cleaner/slow?QUEUECLEANER__SLOW_IGNORE_PRIVATE">QUEUECLEANER__SLOW_IGNORE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/queue-cleaner/slow?QUEUECLEANER__SLOW_DELETE_PRIVATE">QUEUECLEANER__SLOW_DELETE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/queue-cleaner/slow?QUEUECLEANER__SLOW_MIN_SPEED">QUEUECLEANER__SLOW_MIN_SPEED</Link>
{`=1MB
- `}
<Link to="/docs/configuration/queue-cleaner/slow?QUEUECLEANER__SLOW_MAX_TIME">QUEUECLEANER__SLOW_MAX_TIME</Link>
{`=20
- `}
<Link to="/docs/configuration/queue-cleaner/slow?QUEUECLEANER__SLOW_IGNORE_ABOVE_SIZE">QUEUECLEANER__SLOW_IGNORE_ABOVE_SIZE</Link>
{`=60GB
- `}
<Link to="/docs/configuration/content-blocker/general?TRIGGERS__CONTENTBLOCKER">TRIGGERS__CONTENTBLOCKER</Link>
{`=0 0/5 * * * ?
- `}
<Link to="/docs/configuration/content-blocker/general?CONTENTBLOCKER__ENABLED">CONTENTBLOCKER__ENABLED</Link>
{`=true
- `}
<Link to="/docs/configuration/content-blocker/general?CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH">CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH</Link>
{`=/ignored.txt
- `}
<Link to="/docs/configuration/content-blocker/general?CONTENTBLOCKER__IGNORE_PRIVATE">CONTENTBLOCKER__IGNORE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/content-blocker/general?CONTENTBLOCKER__DELETE_PRIVATE">CONTENTBLOCKER__DELETE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/download-cleaner/general?TRIGGERS__DOWNLOADCLEANER">TRIGGERS__DOWNLOADCLEANER</Link>
{`=0 0 * * * ?
- `}
<Link to="/docs/configuration/download-cleaner/general?DOWNLOADCLEANER__ENABLED">DOWNLOADCLEANER__ENABLED</Link>
{`=true
- `}
<Link to="/docs/configuration/download-cleaner/general?DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH">DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH</Link>
{`=/ignored.txt
- `}
<Link to="/docs/configuration/download-cleaner/general?DOWNLOADCLEANER__DELETE_PRIVATE">DOWNLOADCLEANER__DELETE_PRIVATE</Link>
{`=false
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__NAME">DOWNLOADCLEANER__CATEGORIES__0__NAME</Link>
{`=tv-sonarr
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO">DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO</Link>
{`=1
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME">DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME</Link>
{`=0
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME">DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME</Link>
{`=240
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__NAME">DOWNLOADCLEANER__CATEGORIES__1__NAME</Link>
{`=radarr
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO">DOWNLOADCLEANER__CATEGORIES__1__MAX_RATIO</Link>
{`=1
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME">DOWNLOADCLEANER__CATEGORIES__1__MIN_SEED_TIME</Link>
{`=0
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME">DOWNLOADCLEANER__CATEGORIES__1__MAX_SEED_TIME</Link>
{`=240
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__NAME">DOWNLOADCLEANER__CATEGORIES__2__NAME</Link>
{`=cleanuperr-unlinked
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO">DOWNLOADCLEANER__CATEGORIES__2__MAX_RATIO</Link>
{`=1
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME">DOWNLOADCLEANER__CATEGORIES__2__MIN_SEED_TIME</Link>
{`=0
- `}
<Link to="/docs/configuration/download-cleaner/seeding?DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME">DOWNLOADCLEANER__CATEGORIES__2__MAX_SEED_TIME</Link>
{`=240
- `}
<Link to="/docs/configuration/download-cleaner/hardlinks?DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY">DOWNLOADCLEANER__UNLINKED_TARGET_CATEGORY</Link>
{`=cleanuperr-unlinked
- `}
<Link to="/docs/configuration/download-cleaner/hardlinks?DOWNLOADCLEANER__UNLINKED_USE_TAG">DOWNLOADCLEANER__UNLINKED_USE_TAG</Link>
{`=false
- `}
<Link to="/docs/configuration/download-cleaner/hardlinks?DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR">DOWNLOADCLEANER__UNLINKED_IGNORED_ROOT_DIR</Link>
{`=/downloads
- `}
<Link to="/docs/configuration/download-cleaner/hardlinks?DOWNLOADCLEANER__UNLINKED_CATEGORIES__0">DOWNLOADCLEANER__UNLINKED_CATEGORIES__0</Link>
{`=tv-sonarr
- `}
<Link to="/docs/configuration/download-cleaner/hardlinks?DOWNLOADCLEANER__UNLINKED_CATEGORIES__0">DOWNLOADCLEANER__UNLINKED_CATEGORIES__1</Link>
{`=radarr
- `}
<Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">DOWNLOAD_CLIENT</Link>
{`=none
# OR
# - `}
<Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">DOWNLOAD_CLIENT</Link>
{`=disabled
# OR
# - `}
<Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">DOWNLOAD_CLIENT</Link>
{`=qBittorrent
# - `}
<Link to="/docs/configuration/download-client/qbit?QBITTORRENT__URL">QBITTORRENT__URL</Link>
{`=http://localhost:8080
# - `}
<Link to="/docs/configuration/download-client/qbit?QBITTORRENT__URL_BASE">QBITTORRENT__URL_BASE</Link>
{`=myCustomPath
# - `}
<Link to="/docs/configuration/download-client/qbit?QBITTORRENT__USERNAME">QBITTORRENT__USERNAME</Link>
{`=user
# - `}
<Link to="/docs/configuration/download-client/qbit?QBITTORRENT__PASSWORD">QBITTORRENT__PASSWORD</Link>
{`=pass
# OR
# - `}
<Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">DOWNLOAD_CLIENT</Link>
{`=deluge
# - `}
<Link to="/docs/configuration/download-client/deluge?DELUGE__URL">DELUGE__URL</Link>
{`=http://localhost:8112
# - `}
<Link to="/docs/configuration/download-client/deluge?DELUGE__URL_BASE">DELUGE__URL_BASE</Link>
{`=myCustomPath
# - `}
<Link to="/docs/configuration/download-client/deluge?DELUGE__PASSWORD">DELUGE__PASSWORD</Link>
{`=pass
# OR
# - `}
<Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">DOWNLOAD_CLIENT</Link>
{`=transmission
# - `}
<Link to="/docs/configuration/download-client/transmission?TRANSMISSION__URL">TRANSMISSION__URL</Link>
{`=http://localhost:9091
# - `}
<Link to="/docs/configuration/download-client/transmission?TRANSMISSION__URL_BASE">TRANSMISSION__URL_BASE</Link>
{`=myCustomPath
# - `}
<Link to="/docs/configuration/download-client/transmission?TRANSMISSION__USERNAME">TRANSMISSION__USERNAME</Link>
{`=user
# - `}
<Link to="/docs/configuration/download-client/transmission?TRANSMISSION__PASSWORD">TRANSMISSION__PASSWORD</Link>
{`=pass
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__ENABLED">SONARR__ENABLED</Link>
{`=true
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__IMPORT_FAILED_MAX_STRIKES">SONARR__IMPORT_FAILED_MAX_STRIKES</Link>
{`=-1
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__SEARCHTYPE">SONARR__SEARCHTYPE</Link>
{`=Episode
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__BLOCK__TYPE">SONARR__BLOCK__TYPE</Link>
{`=blacklist
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__BLOCK__PATH">SONARR__BLOCK__PATH</Link>
{`=https://example.com/path/to/file.txt
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__INSTANCES__0__URL">SONARR__INSTANCES__0__URL</Link>
{`=http://localhost:8989
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__INSTANCES__0__APIKEY">SONARR__INSTANCES__0__APIKEY</Link>
{`=sonarrSecret1
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__INSTANCES__0__URL">SONARR__INSTANCES__1__URL</Link>
{`=http://localhost:8990
- `}
<Link to="/docs/configuration/arrs/sonarr?SONARR__INSTANCES__0__APIKEY">SONARR__INSTANCES__1__APIKEY</Link>
{`=sonarrSecret2
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__ENABLED">RADARR__ENABLED</Link>
{`=true
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__IMPORT_FAILED_MAX_STRIKES">RADARR__IMPORT_FAILED_MAX_STRIKES</Link>
{`=-1
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__BLOCK__TYPE">RADARR__BLOCK__TYPE</Link>
{`=blacklist
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__BLOCK__PATH">RADARR__BLOCK__PATH</Link>
{`=https://example.com/path/to/file.txt
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__INSTANCES__0__URL">RADARR__INSTANCES__0__URL</Link>
{`=http://localhost:7878
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__INSTANCES__0__APIKEY">RADARR__INSTANCES__0__APIKEY</Link>
{`=radarrSecret1
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__INSTANCES__0__URL">RADARR__INSTANCES__1__URL</Link>
{`=http://localhost:7879
- `}
<Link to="/docs/configuration/arrs/radarr?RADARR__INSTANCES__0__APIKEY">RADARR__INSTANCES__1__APIKEY</Link>
{`=radarrSecret2
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__ENABLED">LIDARR__ENABLED</Link>
{`=true
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__IMPORT_FAILED_MAX_STRIKES">LIDARR__IMPORT_FAILED_MAX_STRIKES</Link>
{`=-1
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__BLOCK__TYPE">LIDARR__BLOCK__TYPE</Link>
{`=blacklist
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__BLOCK__PATH">LIDARR__BLOCK__PATH</Link>
{`=https://example.com/path/to/file.txt
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__INSTANCES__0__URL">LIDARR__INSTANCES__0__URL</Link>
{`=http://localhost:8686
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__INSTANCES__0__APIKEY">LIDARR__INSTANCES__0__APIKEY</Link>
{`=lidarrSecret1
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__INSTANCES__0__URL">LIDARR__INSTANCES__1__URL</Link>
{`=http://localhost:8687
- `}
<Link to="/docs/configuration/arrs/lidarr?LIDARR__INSTANCES__0__APIKEY">LIDARR__INSTANCES__1__APIKEY</Link>
{`=lidarrSecret2
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__ON_IMPORT_FAILED_STRIKE">NOTIFIARR__ON_IMPORT_FAILED_STRIKE</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__ON_STALLED_STRIKE">NOTIFIARR__ON_STALLED_STRIKE</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__ON_SLOW_STRIKE">NOTIFIARR__ON_SLOW_STRIKE</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__ON_QUEUE_ITEM_DELETED">NOTIFIARR__ON_QUEUE_ITEM_DELETED</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__ON_DOWNLOAD_CLEANED">NOTIFIARR__ON_DOWNLOAD_CLEANED</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__ON_CATEGORY_CHANGED">NOTIFIARR__ON_CATEGORY_CHANGED</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__API_KEY">NOTIFIARR__API_KEY</Link>
{`=notifiarrSecret
- `}
<Link to="/docs/configuration/notifications/notifiarr?NOTIFIARR__CHANNEL_ID">NOTIFIARR__CHANNEL_ID</Link>
{`=discordChannelId
- `}
<Link to="/docs/configuration/notifications/apprise?APPRISE__ON_IMPORT_FAILED_STRIKE">APPRISE__ON_IMPORT_FAILED_STRIKE</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/apprise?APPRISE__ON_STALLED_STRIKE">APPRISE__ON_STALLED_STRIKE</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/apprise?APPRISE__ON_SLOW_STRIKE">APPRISE__ON_SLOW_STRIKE</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/apprise?APPRISE__ON_QUEUE_ITEM_DELETED">APPRISE__ON_QUEUE_ITEM_DELETED</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/apprise?APPRISE__ON_DOWNLOAD_CLEANED">APPRISE__ON_DOWNLOAD_CLEANED</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/apprise?APPRISE__ON_CATEGORY_CHANGED">APPRISE__ON_CATEGORY_CHANGED</Link>
{`=true
- `}
<Link to="/docs/configuration/notifications/apprise?APPRISE__URL">APPRISE__URL</Link>
{`=http://apprise:8000
- `}<Link to="/docs/configuration/notifications/apprise?NOTIFIARR__CHANNEL_ID">APPRISE__KEY</Link>
{`=myConfigKey`}
</div>
</CodeBlock>

View File

@@ -1,9 +1,13 @@
import { Note } from '@site/src/components/Admonition';
# Configuration file example (when not using Docker)
# Configuration file (when not using Docker)
<Note>
**This example contains all settings and should be modified to fit your needs.**
This example contains all settings and should be modified to fit your needs.
</Note>
<Note>
Click on an environment variable's name to go to its documentation.
</Note>
```
@@ -20,6 +24,8 @@ import { Note } from '@site/src/components/Admonition';
"Path": "/var/logs"
}
},
"SEARCH_ENABLED": true,
"SEARCH_DELAY": 30,
"Triggers": {
"QueueCleaner": "0 0/5 * * * ?",
"ContentBlocker": "0 0/5 * * * ?",
@@ -78,6 +84,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 +112,7 @@ import { Note } from '@site/src/components/Admonition';
},
"Sonarr": {
"Enabled": true,
"IMPORT_FAILED_MAX_STRIKES=-1
"SearchType": "Episode",
"Block": {
"Type": "blacklist",
@@ -123,6 +131,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 +149,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"

View File

@@ -1,6 +1,6 @@
{
"label": "Configuration examples",
"position": 8,
"label": "Examples",
"position": 1,
"link": {
"type": "generated-index"
}

View File

@@ -0,0 +1,11 @@
[data-theme='light'] code a {
color: inherit;
}
[data-theme='dark'] code a {
color: inherit;
}
code a:hover {
text-decoration: none;
}

View File

@@ -1,8 +1,8 @@
{
"label": "Notifications",
"position": 7,
"position": 8,
"link": {
"type": "generated-index",
"description": "Settings for receiving notifications."
}
}
}

View File

@@ -3,9 +3,15 @@ sidebar_position: 3
---
import QueueCleanerStalledSettings from '@site/src/components/configuration/queue-cleaner/QueueCleanerStalledSettings';
import { Important } from '@site/src/components/Admonition';
import Link from '@docusaurus/Link';
# Stalled Downloads Settings
These settings control how the Queue Cleaner handles stalled downloads.
<Important>
These settings need a <Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">download client</Link> to be configured.
</Important>
<QueueCleanerStalledSettings/>

View File

@@ -3,9 +3,15 @@ sidebar_position: 4
---
import QueueCleanerSlowSettings from '@site/src/components/configuration/queue-cleaner/QueueCleanerSlowSettings';
import { Important } from '@site/src/components/Admonition';
import Link from '@docusaurus/Link';
# Slow Downloads Settings
These settings control how the Queue Cleaner handles slow downloads.
<Important>
These settings need a <Link to="/docs/configuration/download-client/general?DOWNLOAD_CLIENT">download client</Link> to be configured.
</Important>
<QueueCleanerSlowSettings/>

View File

@@ -1,6 +1,6 @@
{
"label": "Queue Cleaner",
"position": 1,
"position": 3,
"link": {
"type": "generated-index",
"description": "Settings for the Queue Cleaner functionality."

View File

@@ -61,7 +61,7 @@ const config: Config = {
position: 'right',
},
{
href: 'https://discord.gg/sWggpnmGNY',
href: 'https://discord.gg/SCtMCgtsc4',
label: 'Discord',
position: 'right',
}
@@ -76,6 +76,28 @@ const config: Config = {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
algolia: {
// The application ID provided by Algolia
appId: 'Y4APRVTFUQ',
apiKey: 'bdaa942f24c8f4ed9893a5b5970405fa',
indexName: 'flmorgio',
// Optional: see doc section below
contextualSearch: true,
// Optional: Algolia search parameters
searchParameters: {},
// Optional: path for search page that enabled by default (`false` to disable it)
searchPagePath: 'search',
// Optional: whether the insights feature is enabled or not on Docsearch (`false` by default)
insights: true,
//... other Algolia params
},
} satisfies Preset.ThemeConfig,
};

View File

@@ -0,0 +1,35 @@
import React from "react";
import EnvVars, { EnvVarProps } from "./EnvVars";
const settings: EnvVarProps[] = [
{
name: "SEARCH_ENABLED",
description: [
"Enabled searching for replacements after a download has been removed from an arr."
],
type: "boolean",
defaultValue: "true",
required: false,
acceptedValues: ["true", "false"],
notes: [
"If you are using [Huntarr](https://github.com/plexguide/Huntarr.io), this setting should be set to false to let Huntarr do the searching.",
]
},
{
name: "SEARCH_DELAY",
description: [
"If searching for replacements is enabled, this setting will delay the searches by the specified number of seconds.",
"This is useful to avoid overwhelming the indexer with too many requests at once.",
],
type: "positive integer number",
defaultValue: "30",
required: false,
important: [
"A lower value or `0` will result in faster searches, but may cause issues such as being rate limited or banned by the indexer.",
]
},
];
export default function SearchSettings() {
return <EnvVars vars={settings} />;
}

View File

@@ -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: [

View File

@@ -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: [

View File

@@ -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: [

View File

@@ -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.",
],
}
];

View File

@@ -0,0 +1,37 @@
import React from "react";
import EnvVars, { EnvVarProps } from "../EnvVars";
const settings: EnvVarProps[] = [
{
name: "DELUGE__URL",
description: [
"URL of the Deluge instance."
],
type: "text",
defaultValue: "http://localhost:8112",
required: false,
examples: ["http://localhost:8112", "http://192.168.1.100:8112", "https://mydomain.com:8112"],
},
{
name: "DELUGE__URL_BASE",
description: [
"Adds a prefix to the deluge json url, such as `[DELUGE__URL]/[DELUGE__URL_BASE]/json`."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "DELUGE__PASSWORD",
description: [
"Password for Deluge authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
},
];
export default function DelugeSettings() {
return <EnvVars vars={settings} />;
}

View File

@@ -19,99 +19,6 @@ const settings: EnvVarProps[] = [
"Setting `DOWNLOAD_CLIENT=disabled` means you don't care about seeding, ratio, H&R and potentially losing your private tracker account."
]
},
{
name: "QBITTORRENT__URL",
description: [
"URL of the qBittorrent instance."
],
type: "text",
defaultValue: "http://localhost:8080",
required: false,
examples: ["http://localhost:8080", "http://192.168.1.100:8080", "https://mydomain.com:8080"],
},
{
name: "QBITTORRENT__URL_BASE",
description: [
"Adds a prefix to the qBittorrent url, such as `[QBITTORRENT__URL]/[QBITTORRENT__URL_BASE]/api`."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "QBITTORRENT__PASSWORD",
description: [
"Password for qBittorrent authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "DELUGE__URL",
description: [
"URL of the Deluge instance."
],
type: "text",
defaultValue: "http://localhost:8112",
required: false,
examples: ["http://localhost:8112", "http://192.168.1.100:8112", "https://mydomain.com:8112"],
},
{
name: "DELUGE__URL_BASE",
description: [
"Adds a prefix to the deluge json url, such as `[DELUGE__URL]/[DELUGE__URL_BASE]/json`."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "DELUGE__PASSWORD",
description: [
"Password for Deluge authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "TRANSMISSION__URL",
description: [
"URL of the Transmission instance."
],
type: "text",
defaultValue: "http://localhost:9091",
required: false,
examples: ["http://localhost:9091", "http://192.168.1.100:9091", "https://mydomain.com:9091"],
},
{
name: "TRANSMISSION__URL_BASE",
description: [
"Adds a prefix to the Transmission rpc url, such as `[TRANSMISSION__URL]/[TRANSMISSION__URL_BASE]/rpc`."
],
type: "text",
defaultValue: "transmission",
required: false,
},
{
name: "TRANSMISSION__USERNAME",
description: [
"Username for Transmission authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "TRANSMISSION__PASSWORD",
description: [
"Password for Transmission authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
}
];
export default function DownloadClientSettings() {

View File

@@ -0,0 +1,46 @@
import React from "react";
import EnvVars, { EnvVarProps } from "../EnvVars";
const settings: EnvVarProps[] = [
{
name: "QBITTORRENT__URL",
description: [
"URL of the qBittorrent instance."
],
type: "text",
defaultValue: "http://localhost:8080",
required: false,
examples: ["http://localhost:8080", "http://192.168.1.100:8080", "https://mydomain.com:8080"],
},
{
name: "QBITTORRENT__URL_BASE",
description: [
"Adds a prefix to the qBittorrent url, such as `[QBITTORRENT__URL]/[QBITTORRENT__URL_BASE]/api`."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "QBITTORRENT__USERNAME",
description: [
"Username for qBittorrent authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "QBITTORRENT__PASSWORD",
description: [
"Password for qBittorrent authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
},
];
export default function QBittorrentSettings() {
return <EnvVars vars={settings} />;
}

View File

@@ -0,0 +1,46 @@
import React from "react";
import EnvVars, { EnvVarProps } from "../EnvVars";
const settings: EnvVarProps[] = [
{
name: "TRANSMISSION__URL",
description: [
"URL of the Transmission instance."
],
type: "text",
defaultValue: "http://localhost:9091",
required: false,
examples: ["http://localhost:9091", "http://192.168.1.100:9091", "https://mydomain.com:9091"],
},
{
name: "TRANSMISSION__URL_BASE",
description: [
"Adds a prefix to the Transmission rpc url, such as `[TRANSMISSION__URL]/[TRANSMISSION__URL_BASE]/rpc`."
],
type: "text",
defaultValue: "transmission",
required: false,
},
{
name: "TRANSMISSION__USERNAME",
description: [
"Username for Transmission authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
},
{
name: "TRANSMISSION__PASSWORD",
description: [
"Password for Transmission authentication."
],
type: "text",
defaultValue: "Empty",
required: false,
}
];
export default function TransmissionSettings() {
return <EnvVars vars={settings} />;
}