mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-04 20:08:08 -05:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a92ebd75c2 | ||
|
|
e6d3929fc9 | ||
|
|
a68e13af35 | ||
|
|
324c3ace8f | ||
|
|
3a9d5d9085 | ||
|
|
89a6eaf0ce | ||
|
|
027c4a0f4d | ||
|
|
81990c6768 | ||
|
|
ba02aa0e49 | ||
|
|
5adbdbd920 | ||
|
|
b3b211d956 | ||
|
|
279bd6d82d | ||
|
|
5dced28228 |
118
README.md
118
README.md
@@ -16,6 +16,7 @@ cleanuperr was created primarily to address malicious files, such as `*.lnk` or
|
||||
> - 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.
|
||||
> - Ignore certain torrent hashes, categories, tags or trackers from processing.
|
||||
|
||||
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.
|
||||
|
||||
@@ -25,15 +26,21 @@ cleanuperr supports both qBittorrent's built-in exclusion features and its own b
|
||||
> https://discord.gg/sWggpnmGNY
|
||||
|
||||
## Table of contents:
|
||||
- [Naming choice](README.md#naming-choice)
|
||||
- [Quick Start](README.md#quick-start)
|
||||
- [How it works](README.md#how-it-works)
|
||||
- [Setup](README.md#setup)
|
||||
- [Usage](README.md#usage)
|
||||
- [Docker Compose](README.md#docker-compose-yaml)
|
||||
- [Environment Variables](README.md#environment-variables)
|
||||
- [Binaries](README.md#binaries-if-youre-not-using-docker)
|
||||
- [Credits](README.md#credits)
|
||||
- [Naming choice](#naming-choice)
|
||||
- [Quick Start](#quick-start)
|
||||
- [How it works](#how-it-works)
|
||||
- [Content blocker](#1-content-blocker-will)
|
||||
- [Queue cleaner](#2-queue-cleaner-will)
|
||||
- [Download cleaner](#3-download-cleaner-will)
|
||||
- [Setup](#setup-examples)
|
||||
- [Usage](#usage)
|
||||
- [Docker](#docker)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Docker Compose](#docker-compose-example)
|
||||
- [Windows](#windows)
|
||||
- [Linux](#linux)
|
||||
- [MacOS](#macos)
|
||||
- [Credits](#credits)
|
||||
|
||||
## Naming choice
|
||||
|
||||
@@ -52,10 +59,10 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
||||
> Use the Unraid Community App.
|
||||
>
|
||||
> 3. **Manual Installation (if you're not using Docker)**
|
||||
> More details [here](#binaries-if-youre-not-using-docker).
|
||||
> Go to [Windows](#windows), [Linux](#linux) or [MacOS](#macos).
|
||||
|
||||
> [!TIP]
|
||||
> Refer to the [Environment variables](#Environment-variables) section for detailed configuration instructions and the [Setup](#Setup) section for an in-depth explanation of the cleanup process.
|
||||
> Refer to the [Environment variables](#environment-variables) section for detailed configuration instructions and the [Setup examples](#setup-examples) section for an in-depth explanation of the cleanup process.
|
||||
|
||||
|
||||
> [!IMPORTANT]
|
||||
@@ -69,7 +76,7 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
||||
|
||||
# How it works
|
||||
|
||||
1. **Content blocker** will:
|
||||
#### 1. **Content blocker** will:
|
||||
- Run every 5 minutes (or configured cron).
|
||||
- Process all items in the *arr queue.
|
||||
- Find the corresponding item from the download client for each queue item.
|
||||
@@ -80,7 +87,7 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
||||
- It will be removed from the *arr's queue and blocked.
|
||||
- It will be deleted from the download client.
|
||||
- A new search will be triggered for the *arr item.
|
||||
2. **Queue cleaner** will:
|
||||
#### 2. **Queue cleaner** will:
|
||||
- Run every 5 minutes (or configured cron, or right after `content blocker`).
|
||||
- Process all items in the *arr queue.
|
||||
- Check each queue item if it is **stalled (download speed is 0)**, **stuck in metadata downloading** or **failed to be imported**.
|
||||
@@ -93,11 +100,11 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
||||
- It will be removed from the *arr's queue and blocked.
|
||||
- It will be deleted from the download client.
|
||||
- A new search will be triggered for the *arr item.
|
||||
3. **Download cleaner** will:
|
||||
#### 3. **Download cleaner** will:
|
||||
- Run every hour (or configured cron).
|
||||
- Automatically clean up downloads that have been seeding for a certain amount of time.
|
||||
|
||||
# Setup
|
||||
# Setup examples
|
||||
|
||||
## Using qBittorrent's built-in feature (works only with qBittorrent)
|
||||
|
||||
@@ -112,7 +119,7 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
||||
## Using cleanuperr's blocklist (works with all supported download clients)
|
||||
|
||||
1. Set both `QUEUECLEANER__ENABLED` and `CONTENTBLOCKER__ENABLED` to `true` in your environment variables.
|
||||
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Arr variables](#Arr-variables) section.
|
||||
2. Configure and enable either a **blacklist** or a **whitelist** as described in the [Arr variables](variables.md#Arr-settings) section.
|
||||
3. Once configured, cleanuperr will perform the following tasks:
|
||||
- Execute the **content blocker** job, as explained in the [How it works](#how-it-works) section.
|
||||
- Execute the **queue cleaner** job, as explained in the [How it works](#how-it-works) section.
|
||||
@@ -129,7 +136,26 @@ I've seen a few discussions on this type of naming and I've decided that I didn'
|
||||
|
||||
## Usage
|
||||
|
||||
### Docker compose yaml
|
||||
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/docker.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Docker</span>
|
||||
|
||||
|
||||
### **Environment variables**
|
||||
|
||||
**Jump to:**
|
||||
- [General settings](variables.md#general-settings)
|
||||
- [Queue Cleaner settings](variables.md#queue-cleaner-settings)
|
||||
- [Content Blocker settings](variables.md#content-blocker-settings)
|
||||
- [Download Cleaner settings](variables.md#download-cleaner-settings)
|
||||
- [Download Client settings](variables.md#download-client-settings)
|
||||
- [Arr settings](variables.md#arr-settings)
|
||||
- [Notification settings](variables.md#notification-settings)
|
||||
- [Advanced settings](variables.md#advanced-settings)
|
||||
|
||||
### Docker compose example
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This example contains all settings and should be modified to fit your needs.
|
||||
|
||||
```
|
||||
version: "3.3"
|
||||
@@ -139,7 +165,9 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./cleanuperr/logs:/var/logs
|
||||
- ./cleanuperr/ignored.txt:/ignored.txt
|
||||
environment:
|
||||
- TZ=America/New_York
|
||||
- DRY_RUN=false
|
||||
|
||||
- LOGGING__LOGLEVEL=Information
|
||||
@@ -152,6 +180,7 @@ services:
|
||||
- TRIGGERS__DOWNLOADCLEANER=0 0 * * * ?
|
||||
|
||||
- QUEUECLEANER__ENABLED=true
|
||||
- QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
|
||||
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
||||
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
||||
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
|
||||
@@ -164,10 +193,12 @@ services:
|
||||
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
||||
|
||||
- CONTENTBLOCKER__ENABLED=true
|
||||
- CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored.txt
|
||||
- CONTENTBLOCKER__IGNORE_PRIVATE=false
|
||||
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
||||
|
||||
- DOWNLOADCLEANER__ENABLED=true
|
||||
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored.txt
|
||||
- DOWNLOADCLEANER__DELETE_PRIVATE=false
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
|
||||
@@ -227,28 +258,48 @@ services:
|
||||
- NOTIFIARR__CHANNEL_ID=discord_channel_id
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/windows.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Windows</span>
|
||||
|
||||
Jump to:
|
||||
- [General settings](variables.md#general-settings)
|
||||
- [Queue Cleaner settings](variables.md#queue-cleaner-settings)
|
||||
- [Content Blocker settings](variables.md#content-blocker-settings)
|
||||
- [Download Cleaner settings](variables.md#download-cleaner-settings)
|
||||
- [Download Client settings](variables.md#download-client-settings)
|
||||
- [Arr settings](variables.md#arr-settings)
|
||||
- [Notification settings](variables.md#notification-settings)
|
||||
- [Advanced settings](variables.md#advanced-settings)
|
||||
|
||||
### Binaries (if you're not using Docker)
|
||||
|
||||
1. Download the binaries from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||
2. Extract them from the zip file.
|
||||
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [above](#environment-variables).
|
||||
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||
2. Extract the zip file into `C:\example\directory`.
|
||||
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
|
||||
4. Execute `cleanuperr.exe`.
|
||||
|
||||
> [!TIP]
|
||||
> ### Run as a Windows Service
|
||||
> Check out this stackoverflow answer on how to do it: https://stackoverflow.com/a/15719678
|
||||
|
||||
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/linux.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Linux</span>
|
||||
|
||||
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||
2. Extract the zip file into `/example/directory`.
|
||||
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
|
||||
4. Open a terminal and execute these commands:
|
||||
```
|
||||
cd /example/directory
|
||||
chmod +x cleanuperr
|
||||
./cleanuperr
|
||||
```
|
||||
|
||||
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/apple.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">MacOS</span>
|
||||
|
||||
1. Download the zip file from [releases](https://github.com/flmorg/cleanuperr/releases).
|
||||
2. Extract the zip file into `/example/directory`.
|
||||
3. Edit **appsettings.json**. The paths from this json file correspond with the docker env vars, as described [here](#environment-variables).
|
||||
4. Open a terminal and execute these commands:
|
||||
```
|
||||
cd /example/directory
|
||||
chmod +x cleanuperr
|
||||
./cleanuperr
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Some people have experienced problems when trying to execute cleanuperr on MacOS because the system actively blocked the file for not being signed.
|
||||
> As per [this](), you may need to also execute this command:
|
||||
> ```
|
||||
> codesign --sign - --force --preserve-metadata=entitlements,requirements,flags,runtime /example/directory/cleanuperr
|
||||
> ```
|
||||
|
||||
# Credits
|
||||
Special thanks for inspiration go to:
|
||||
- [ThijmenGThN/swaparr](https://github.com/ThijmenGThN/swaparr)
|
||||
@@ -260,4 +311,3 @@ Special thanks for inspiration go to:
|
||||
If I made your life just a tiny bit easier, consider buying me a coffee!
|
||||
|
||||
<a href="https://buymeacoffee.com/flaminel" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Common.Configuration.ContentBlocker;
|
||||
|
||||
public sealed record ContentBlockerConfig : IJobConfig
|
||||
public sealed record ContentBlockerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||
{
|
||||
public const string SectionName = "ContentBlocker";
|
||||
|
||||
@@ -13,6 +13,9 @@ public sealed record ContentBlockerConfig : IJobConfig
|
||||
|
||||
[ConfigurationKeyName("DELETE_PRIVATE")]
|
||||
public bool DeletePrivate { get; init; }
|
||||
|
||||
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
|
||||
public string? IgnoredDownloadsPath { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Common.Configuration.DownloadCleaner;
|
||||
|
||||
public sealed record DownloadCleanerConfig : IJobConfig
|
||||
public sealed record DownloadCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||
{
|
||||
public const string SectionName = "DownloadCleaner";
|
||||
|
||||
@@ -12,7 +12,10 @@ public sealed record DownloadCleanerConfig : IJobConfig
|
||||
public List<Category>? Categories { get; init; }
|
||||
|
||||
[ConfigurationKeyName("DELETE_PRIVATE")]
|
||||
public bool DeletePrivate { get; set; }
|
||||
public bool DeletePrivate { get; init; }
|
||||
|
||||
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
|
||||
public string? IgnoredDownloadsPath { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
|
||||
6
code/Common/Configuration/IIgnoredDownloadsConfig.cs
Normal file
6
code/Common/Configuration/IIgnoredDownloadsConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Common.Configuration;
|
||||
|
||||
public interface IIgnoredDownloadsConfig
|
||||
{
|
||||
string? IgnoredDownloadsPath { get; }
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Common.Exceptions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Common.Configuration.QueueCleaner;
|
||||
|
||||
public sealed record QueueCleanerConfig : IJobConfig
|
||||
public sealed record QueueCleanerConfig : IJobConfig, IIgnoredDownloadsConfig
|
||||
{
|
||||
public const string SectionName = "QueueCleaner";
|
||||
|
||||
@@ -10,6 +11,9 @@ public sealed record QueueCleanerConfig : IJobConfig
|
||||
|
||||
public required bool RunSequentially { get; init; }
|
||||
|
||||
[ConfigurationKeyName("IGNORED_DOWNLOADS_PATH")]
|
||||
public string? IgnoredDownloadsPath { get; init; }
|
||||
|
||||
[ConfigurationKeyName("IMPORT_FAILED_MAX_STRIKES")]
|
||||
public ushort ImportFailedMaxStrikes { get; init; }
|
||||
|
||||
@@ -36,5 +40,14 @@ public sealed record QueueCleanerConfig : IJobConfig
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (ImportFailedMaxStrikes is > 0 and < 3)
|
||||
{
|
||||
throw new ValidationException("the minimum value for IMPORT_FAILED_MAX_STRIKES must be 3");
|
||||
}
|
||||
|
||||
if (StalledMaxStrikes is > 0 and < 3)
|
||||
{
|
||||
throw new ValidationException("the minimum value for STALLED_MAX_STRIKES must be 3");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,4 +23,11 @@ public sealed record TorrentStatus
|
||||
public long SeedingTime { get; init; }
|
||||
|
||||
public float Ratio { get; init; }
|
||||
|
||||
public required IReadOnlyList<Tracker> Trackers { get; init; }
|
||||
}
|
||||
|
||||
public sealed record Tracker
|
||||
{
|
||||
public required Uri Url { get; init; }
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public static class MainDI
|
||||
services
|
||||
.AddHttpClient(nameof(DelugeService), x =>
|
||||
{
|
||||
x.Timeout = TimeSpan.FromSeconds(5);
|
||||
x.Timeout = TimeSpan.FromSeconds(config.Timeout);
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(_ =>
|
||||
{
|
||||
|
||||
@@ -55,7 +55,7 @@ public static class QuartzDI
|
||||
if (contentBlockerConfig?.Enabled is true && queueCleanerConfig is { Enabled: true, RunSequentially: true })
|
||||
{
|
||||
q.AddJob<QueueCleaner>(queueCleanerConfig, string.Empty);
|
||||
q.AddJobListener(new JobChainingListener(nameof(QueueCleaner)));
|
||||
q.AddJobListener(new JobChainingListener(nameof(ContentBlocker), nameof(QueueCleaner)));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
using Infrastructure.Interceptors;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Common.Configuration.DownloadCleaner;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Infrastructure.Interceptors;
|
||||
using Infrastructure.Providers;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.DownloadCleaner;
|
||||
@@ -30,5 +34,8 @@ public static class ServicesDI
|
||||
.AddTransient<ArrQueueIterator>()
|
||||
.AddTransient<DownloadServiceFactory>()
|
||||
.AddSingleton<BlocklistProvider>()
|
||||
.AddSingleton<IStriker, Striker>();
|
||||
.AddSingleton<IStriker, Striker>()
|
||||
.AddSingleton<IgnoredDownloadsProvider<QueueCleanerConfig>>()
|
||||
.AddSingleton<IgnoredDownloadsProvider<ContentBlockerConfig>>()
|
||||
.AddSingleton<IgnoredDownloadsProvider<DownloadCleanerConfig>>();
|
||||
}
|
||||
23
code/Executable/HostExtensions.cs
Normal file
23
code/Executable/HostExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace Executable;
|
||||
|
||||
public static class HostExtensions
|
||||
{
|
||||
public static IHost Init(this IHost host)
|
||||
{
|
||||
ILogger<Program> logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
Version? version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
|
||||
logger.LogInformation(
|
||||
version is null
|
||||
? "cleanuperr version not detected"
|
||||
: $"cleanuperr v{version.Major}.{version.Minor}.{version.Build}"
|
||||
);
|
||||
|
||||
logger.LogInformation("timezone: {tz}", TimeZoneInfo.Local.DisplayName);
|
||||
|
||||
return host;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
using Executable;
|
||||
using Executable.DependencyInjection;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
@@ -7,15 +7,6 @@ builder.Services.AddInfrastructure(builder.Configuration);
|
||||
builder.Logging.AddLogging(builder.Configuration);
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
var logger = host.Services.GetRequiredService<ILogger<Program>>();
|
||||
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
|
||||
logger.LogInformation(
|
||||
version is null
|
||||
? "cleanuperr version not detected"
|
||||
: $"cleanuperr v{version.Major}.{version.Minor}.{version.Build}"
|
||||
);
|
||||
host.Init();
|
||||
|
||||
host.Run();
|
||||
@@ -18,11 +18,13 @@
|
||||
"ContentBlocker": {
|
||||
"Enabled": true,
|
||||
"IGNORE_PRIVATE": true,
|
||||
"DELETE_PRIVATE": false
|
||||
"DELETE_PRIVATE": false,
|
||||
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
|
||||
},
|
||||
"QueueCleaner": {
|
||||
"Enabled": true,
|
||||
"RunSequentially": true,
|
||||
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads",
|
||||
"IMPORT_FAILED_MAX_STRIKES": 5,
|
||||
"IMPORT_FAILED_IGNORE_PRIVATE": true,
|
||||
"IMPORT_FAILED_DELETE_PRIVATE": false,
|
||||
@@ -44,7 +46,8 @@
|
||||
"MIN_SEED_TIME": 0,
|
||||
"MAX_SEED_TIME": -1
|
||||
}
|
||||
]
|
||||
],
|
||||
"IGNORED_DOWNLOADS_PATH": "../test/data/cleanuperr/ignored_downloads"
|
||||
},
|
||||
"DOWNLOAD_CLIENT": "qbittorrent",
|
||||
"qBittorrent": {
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
},
|
||||
"ContentBlocker": {
|
||||
"Enabled": false,
|
||||
"IGNORE_PRIVATE": false
|
||||
"IGNORE_PRIVATE": false,
|
||||
"IGNORED_DOWNLOADS_PATH": ""
|
||||
},
|
||||
"QueueCleaner": {
|
||||
"Enabled": true,
|
||||
"RunSequentially": true,
|
||||
"IGNORED_DOWNLOADS_PATH": "",
|
||||
"IMPORT_FAILED_MAX_STRIKES": 0,
|
||||
"IMPORT_FAILED_IGNORE_PRIVATE": false,
|
||||
"IMPORT_FAILED_DELETE_PRIVATE": false,
|
||||
@@ -34,7 +36,8 @@
|
||||
"DownloadCleaner": {
|
||||
"Enabled": false,
|
||||
"DELETE_PRIVATE": false,
|
||||
"CATEGORIES": []
|
||||
"CATEGORIES": [],
|
||||
"IGNORED_DOWNLOADS_PATH": ""
|
||||
},
|
||||
"DOWNLOAD_CLIENT": "none",
|
||||
"qBittorrent": {
|
||||
|
||||
@@ -35,12 +35,13 @@ public class TestDownloadService : DownloadService
|
||||
|
||||
public override void Dispose() { }
|
||||
public override Task LoginAsync() => Task.CompletedTask;
|
||||
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash) => Task.FromResult(new StalledResult());
|
||||
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes) => Task.FromResult(new BlockFilesResult());
|
||||
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads) => Task.FromResult(new StalledResult());
|
||||
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads) => Task.FromResult(new BlockFilesResult());
|
||||
public override Task DeleteDownload(string hash) => Task.CompletedTask;
|
||||
public override Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories) => Task.FromResult<List<object>?>(null);
|
||||
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes) => Task.CompletedTask;
|
||||
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||
IReadOnlyList<string> ignoredDownloads) => Task.CompletedTask;
|
||||
|
||||
// Expose protected methods for testing
|
||||
public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded);
|
||||
|
||||
29
code/Infrastructure/Extensions/DelugeExtensions.cs
Normal file
29
code/Infrastructure/Extensions/DelugeExtensions.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Domain.Models.Deluge.Response;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
public static class DelugeExtensions
|
||||
{
|
||||
public static bool ShouldIgnore(this TorrentStatus download, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
foreach (string value in ignoredDownloads)
|
||||
{
|
||||
if (download.Hash?.Equals(value, StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (download.Label?.Equals(value, StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (download.Trackers.Any(x => x.Url.Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
42
code/Infrastructure/Extensions/QBitExtensions.cs
Normal file
42
code/Infrastructure/Extensions/QBitExtensions.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
public static class QBitExtensions
|
||||
{
|
||||
public static bool ShouldIgnore(this TorrentInfo download, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
foreach (string value in ignoredDownloads)
|
||||
{
|
||||
if (download.Hash.Equals(value, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (download.Category.Equals(value, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (download.Tags.Contains(value, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool ShouldIgnore(this TorrentTracker tracker, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
foreach (string value in ignoredDownloads)
|
||||
{
|
||||
if (tracker.Url.Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
42
code/Infrastructure/Extensions/TransmissionExtensions.cs
Normal file
42
code/Infrastructure/Extensions/TransmissionExtensions.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Infrastructure.Extensions;
|
||||
|
||||
public static class TransmissionExtensions
|
||||
{
|
||||
public static bool ShouldIgnore(this TorrentInfo download, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
foreach (string value in ignoredDownloads)
|
||||
{
|
||||
if (download.HashString?.Equals(value, StringComparison.InvariantCultureIgnoreCase) ?? false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (download.GetCategory().Equals(value, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool? hasIgnoredTracker = download.Trackers?
|
||||
.Any(x => new Uri(x.Announce!).Host.EndsWith(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (hasIgnoredTracker is true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static string GetCategory(this TorrentInfo download)
|
||||
{
|
||||
if (string.IsNullOrEmpty(download.DownloadDir))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return Path.GetFileName(Path.TrimEndingDirectorySeparator(download.DownloadDir));
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,6 @@ public static class CacheKeys
|
||||
public static string BlocklistRegexes(InstanceType instanceType) => $"{instanceType.ToString()}_regexes";
|
||||
|
||||
public static string Item(string hash) => $"item_{hash}";
|
||||
|
||||
public static string IgnoredDownloads(string name) => $"{name}_ignored";
|
||||
}
|
||||
82
code/Infrastructure/Providers/IgnoredDownloadsProvider.cs
Normal file
82
code/Infrastructure/Providers/IgnoredDownloadsProvider.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using Common.Configuration;
|
||||
using Infrastructure.Helpers;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Providers;
|
||||
|
||||
public sealed class IgnoredDownloadsProvider<T>
|
||||
where T : IIgnoredDownloadsConfig
|
||||
{
|
||||
private readonly ILogger<IgnoredDownloadsProvider<T>> _logger;
|
||||
private IIgnoredDownloadsConfig _config;
|
||||
private readonly IMemoryCache _cache;
|
||||
private DateTime _lastModified = DateTime.MinValue;
|
||||
|
||||
public IgnoredDownloadsProvider(ILogger<IgnoredDownloadsProvider<T>> logger, IOptionsMonitor<T> config, IMemoryCache cache)
|
||||
{
|
||||
_config = config.CurrentValue;
|
||||
config.OnChange((newValue) => _config = newValue);
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
|
||||
if (string.IsNullOrEmpty(_config.IgnoredDownloadsPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(_config.IgnoredDownloadsPath))
|
||||
{
|
||||
throw new FileNotFoundException("file not found", _config.IgnoredDownloadsPath);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<string>> GetIgnoredDownloads()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_config.IgnoredDownloadsPath))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
FileInfo fileInfo = new(_config.IgnoredDownloadsPath);
|
||||
|
||||
if (fileInfo.LastWriteTime > _lastModified ||
|
||||
!_cache.TryGetValue(CacheKeys.IgnoredDownloads(typeof(T).Name), out IReadOnlyList<string>? ignoredDownloads) ||
|
||||
ignoredDownloads is null)
|
||||
{
|
||||
_lastModified = fileInfo.LastWriteTime;
|
||||
|
||||
return await LoadFile();
|
||||
}
|
||||
|
||||
return ignoredDownloads;
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<string>> LoadFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(_config.IgnoredDownloadsPath))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
string[] ignoredDownloads = (await File.ReadAllLinesAsync(_config.IgnoredDownloadsPath))
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.ToArray();
|
||||
|
||||
_cache.Set(CacheKeys.IgnoredDownloads(typeof(T).Name), ignoredDownloads);
|
||||
|
||||
_logger.LogInformation("ignored downloads reloaded");
|
||||
|
||||
return ignoredDownloads;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "error while reading ignored downloads file | {file}", _config.IgnoredDownloadsPath);
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -49,9 +49,9 @@ public sealed class BlocklistProvider
|
||||
|
||||
try
|
||||
{
|
||||
await LoadPatternsAndRegexesAsync(_sonarrConfig.Block.Type, _sonarrConfig.Block.Path, InstanceType.Sonarr);
|
||||
await LoadPatternsAndRegexesAsync(_radarrConfig.Block.Type, _radarrConfig.Block.Path, InstanceType.Radarr);
|
||||
await LoadPatternsAndRegexesAsync(_lidarrConfig.Block.Type, _lidarrConfig.Block.Path, InstanceType.Lidarr);
|
||||
await LoadPatternsAndRegexesAsync(_sonarrConfig, InstanceType.Sonarr);
|
||||
await LoadPatternsAndRegexesAsync(_radarrConfig, InstanceType.Radarr);
|
||||
await LoadPatternsAndRegexesAsync(_lidarrConfig, InstanceType.Lidarr);
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
@@ -83,14 +83,19 @@ public sealed class BlocklistProvider
|
||||
return regexes ?? [];
|
||||
}
|
||||
|
||||
private async Task LoadPatternsAndRegexesAsync(BlocklistType blocklistType, string? blocklistPath, InstanceType instanceType)
|
||||
private async Task LoadPatternsAndRegexesAsync(ArrConfig arrConfig, InstanceType instanceType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(blocklistPath))
|
||||
if (!arrConfig.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] filePatterns = await ReadContentAsync(blocklistPath);
|
||||
if (string.IsNullOrEmpty(arrConfig.Block.Path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] filePatterns = await ReadContentAsync(arrConfig.Block.Path);
|
||||
|
||||
long startTime = Stopwatch.GetTimestamp();
|
||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||
@@ -121,13 +126,13 @@ public sealed class BlocklistProvider
|
||||
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
|
||||
_cache.Set(CacheKeys.BlocklistType(instanceType), blocklistType);
|
||||
_cache.Set(CacheKeys.BlocklistType(instanceType), arrConfig.Block.Type);
|
||||
_cache.Set(CacheKeys.BlocklistPatterns(instanceType), patterns);
|
||||
_cache.Set(CacheKeys.BlocklistRegexes(instanceType), regexes);
|
||||
|
||||
_logger.LogDebug("loaded {count} patterns", patterns.Count);
|
||||
_logger.LogDebug("loaded {count} regexes", regexes.Count);
|
||||
_logger.LogDebug("blocklist loaded in {elapsed} ms | {path}", elapsed.TotalMilliseconds, blocklistPath);
|
||||
_logger.LogDebug("blocklist loaded in {elapsed} ms | {path}", elapsed.TotalMilliseconds, arrConfig.Block.Path);
|
||||
}
|
||||
|
||||
private async Task<string[]> ReadContentAsync(string path)
|
||||
|
||||
@@ -6,6 +6,7 @@ using Common.Configuration.DownloadClient;
|
||||
using Domain.Enums;
|
||||
using Domain.Models.Arr;
|
||||
using Domain.Models.Arr.Queue;
|
||||
using Infrastructure.Providers;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.Arr.Interfaces;
|
||||
using Infrastructure.Verticals.Context;
|
||||
@@ -22,7 +23,8 @@ public sealed class ContentBlocker : GenericHandler
|
||||
{
|
||||
private readonly ContentBlockerConfig _config;
|
||||
private readonly BlocklistProvider _blocklistProvider;
|
||||
|
||||
private readonly IgnoredDownloadsProvider<ContentBlockerConfig> _ignoredDownloadsProvider;
|
||||
|
||||
public ContentBlocker(
|
||||
ILogger<ContentBlocker> logger,
|
||||
IOptions<ContentBlockerConfig> config,
|
||||
@@ -36,7 +38,8 @@ public sealed class ContentBlocker : GenericHandler
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
BlocklistProvider blocklistProvider,
|
||||
DownloadServiceFactory downloadServiceFactory,
|
||||
INotificationPublisher notifier
|
||||
INotificationPublisher notifier,
|
||||
IgnoredDownloadsProvider<ContentBlockerConfig> ignoredDownloadsProvider
|
||||
) : base(
|
||||
logger, downloadClientConfig,
|
||||
sonarrConfig, radarrConfig, lidarrConfig,
|
||||
@@ -47,6 +50,7 @@ public sealed class ContentBlocker : GenericHandler
|
||||
{
|
||||
_config = config.Value;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
_ignoredDownloadsProvider = ignoredDownloadsProvider;
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync()
|
||||
@@ -73,6 +77,8 @@ public sealed class ContentBlocker : GenericHandler
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
{
|
||||
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||
|
||||
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
||||
|
||||
HashSet<SearchItem> itemsToBeRefreshed = [];
|
||||
@@ -106,13 +112,19 @@ public sealed class ContentBlocker : GenericHandler
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredDownloads.Contains(record.DownloadId, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("skip | {title} | ignored", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
// push record to context
|
||||
ContextProvider.Set(nameof(QueueRecord), record);
|
||||
|
||||
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
||||
|
||||
BlockFilesResult result = await _downloadService
|
||||
.BlockUnwantedFilesAsync(record.DownloadId, blocklistType, patterns, regexes);
|
||||
.BlockUnwantedFilesAsync(record.DownloadId, blocklistType, patterns, regexes, ignoredDownloads);
|
||||
|
||||
if (!result.ShouldRemove)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ using Common.Configuration.DownloadCleaner;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Domain.Enums;
|
||||
using Domain.Models.Arr.Queue;
|
||||
using Infrastructure.Providers;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.Arr.Interfaces;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
@@ -17,6 +18,7 @@ namespace Infrastructure.Verticals.DownloadCleaner;
|
||||
public sealed class DownloadCleaner : GenericHandler
|
||||
{
|
||||
private readonly DownloadCleanerConfig _config;
|
||||
private readonly IgnoredDownloadsProvider<DownloadCleanerConfig> _ignoredDownloadsProvider;
|
||||
private readonly HashSet<string> _excludedHashes = [];
|
||||
|
||||
public DownloadCleaner(
|
||||
@@ -31,7 +33,8 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
LidarrClient lidarrClient,
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
DownloadServiceFactory downloadServiceFactory,
|
||||
INotificationPublisher notifier
|
||||
INotificationPublisher notifier,
|
||||
IgnoredDownloadsProvider<DownloadCleanerConfig> ignoredDownloadsProvider
|
||||
) : base(
|
||||
logger, downloadClientConfig,
|
||||
sonarrConfig, radarrConfig, lidarrConfig,
|
||||
@@ -42,16 +45,25 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
{
|
||||
_config = config.Value;
|
||||
_config.Validate();
|
||||
_ignoredDownloadsProvider = ignoredDownloadsProvider;
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync()
|
||||
{
|
||||
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
||||
{
|
||||
_logger.LogWarning("download client is set to none");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_config.Categories?.Count is null or 0)
|
||||
{
|
||||
_logger.LogWarning("no categories configured");
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||
|
||||
await _downloadService.LoginAsync();
|
||||
|
||||
List<object>? downloads = await _downloadService.GetAllDownloadsToBeCleaned(_config.Categories);
|
||||
@@ -69,7 +81,7 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr, true);
|
||||
await ProcessArrConfigAsync(_lidarrConfig, InstanceType.Lidarr, true);
|
||||
|
||||
await _downloadService.CleanDownloads(downloads, _config.Categories, _excludedHashes);
|
||||
await _downloadService.CleanDownloads(downloads, _config.Categories, _excludedHashes, ignoredDownloads);
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
|
||||
@@ -16,6 +16,20 @@ public sealed class DelugeClient
|
||||
private readonly DelugeConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private static readonly IReadOnlyList<string> Fields =
|
||||
[
|
||||
"hash",
|
||||
"state",
|
||||
"name",
|
||||
"eta",
|
||||
"private",
|
||||
"total_done",
|
||||
"label",
|
||||
"seeding_time",
|
||||
"ratio",
|
||||
"trackers"
|
||||
];
|
||||
|
||||
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_config = config.Value;
|
||||
@@ -68,7 +82,7 @@ public sealed class DelugeClient
|
||||
return await SendRequest<TorrentStatus?>(
|
||||
"web.get_torrent_status",
|
||||
hash,
|
||||
new[] { "hash", "state", "name", "eta", "private", "total_done", "label", "seeding_time", "ratio" }
|
||||
Fields
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +91,7 @@ public sealed class DelugeClient
|
||||
Dictionary<string, TorrentStatus>? downloads = await SendRequest<Dictionary<string, TorrentStatus>?>(
|
||||
"core.get_torrents_status",
|
||||
"",
|
||||
new[] { "hash", "state", "name", "eta", "private", "total_done", "label", "seeding_time", "ratio" }
|
||||
Fields
|
||||
);
|
||||
|
||||
return downloads?.Values.ToList();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Domain.Enums;
|
||||
using Domain.Models.Deluge.Response;
|
||||
using Infrastructure.Extensions;
|
||||
using Infrastructure.Interceptors;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.Context;
|
||||
@@ -49,20 +50,26 @@ public class DelugeService : DownloadService, IDelugeService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
DelugeContents? contents = null;
|
||||
StalledResult result = new();
|
||||
|
||||
TorrentStatus? status = await _client.GetTorrentStatus(hash);
|
||||
TorrentStatus? download = await _client.GetTorrentStatus(hash);
|
||||
|
||||
if (status?.Hash is null)
|
||||
if (download?.Hash is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -88,8 +95,8 @@ public class DelugeService : DownloadService, IDelugeService
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(status);
|
||||
result.IsPrivate = status.Private;
|
||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download);
|
||||
result.IsPrivate = download.Private;
|
||||
|
||||
if (!shouldRemove && result.ShouldRemove)
|
||||
{
|
||||
@@ -100,30 +107,37 @@ public class DelugeService : DownloadService, IDelugeService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||
string hash,
|
||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||
BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns,
|
||||
ConcurrentBag<Regex> regexes
|
||||
)
|
||||
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
TorrentStatus? status = await _client.GetTorrentStatus(hash);
|
||||
TorrentStatus? download = await _client.GetTorrentStatus(hash);
|
||||
BlockFilesResult result = new();
|
||||
|
||||
if (status?.Hash is null)
|
||||
if (download?.Hash is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.IsPrivate = status.Private;
|
||||
|
||||
if (_contentBlockerConfig.IgnorePrivate && status.Private)
|
||||
var ceva = await _client.GetTorrentExtended(hash);
|
||||
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.IsPrivate = download.Private;
|
||||
|
||||
if (_contentBlockerConfig.IgnorePrivate && download.Private)
|
||||
{
|
||||
// ignore private trackers
|
||||
_logger.LogDebug("skip files check | download is private | {name}", status.Name);
|
||||
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -205,7 +219,8 @@ public class DelugeService : DownloadService, IDelugeService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||
IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
foreach (TorrentStatus download in downloads)
|
||||
{
|
||||
@@ -213,6 +228,12 @@ public class DelugeService : DownloadService, IDelugeService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
Category? category = categoriesToClean
|
||||
.FirstOrDefault(x => x.Name.Equals(download.Label, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
@@ -60,15 +60,13 @@ public abstract class DownloadService : IDownloadService
|
||||
|
||||
public abstract Task LoginAsync();
|
||||
|
||||
public abstract Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
|
||||
public abstract Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||
string hash,
|
||||
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||
BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns,
|
||||
ConcurrentBag<Regex> regexes
|
||||
);
|
||||
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task DeleteDownload(string hash);
|
||||
@@ -77,7 +75,8 @@ public abstract class DownloadService : IDownloadService
|
||||
public abstract Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes);
|
||||
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||
IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
protected void ResetStrikesOnProgress(string hash, long downloaded)
|
||||
{
|
||||
@@ -131,7 +130,7 @@ public abstract class DownloadService : IDownloadService
|
||||
|
||||
return new();
|
||||
}
|
||||
|
||||
|
||||
private bool DownloadReachedRatio(double ratio, TimeSpan seedingTime, Category category)
|
||||
{
|
||||
if (category.MaxRatio < 0)
|
||||
|
||||
@@ -28,12 +28,13 @@ public class DummyDownloadService : DownloadService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType, ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes)
|
||||
public override Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType, ConcurrentBag<string> patterns,
|
||||
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@@ -43,7 +44,8 @@ public class DummyDownloadService : DownloadService
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
||||
public override Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||
IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ public interface IDownloadService : IDisposable
|
||||
/// Checks whether the download should be removed from the *arr queue.
|
||||
/// </summary>
|
||||
/// <param name="hash">The download hash.</param>
|
||||
public Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
|
||||
/// <param name="ignoredDownloads">Downloads to ignore from processing.</param>
|
||||
public Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
/// <summary>
|
||||
/// Blocks unwanted files from being fully downloaded.
|
||||
@@ -23,12 +24,13 @@ public interface IDownloadService : IDisposable
|
||||
/// <param name="blocklistType">The <see cref="BlocklistType"/>.</param>
|
||||
/// <param name="patterns">The patterns to test the files against.</param>
|
||||
/// <param name="regexes">The regexes to test the files against.</param>
|
||||
/// <param name="ignoredDownloads">Downloads to ignore from processing.</param>
|
||||
/// <returns>True if all files have been blocked; otherwise false.</returns>
|
||||
public Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||
string hash,
|
||||
public Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||
BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns,
|
||||
ConcurrentBag<Regex> regexes
|
||||
ConcurrentBag<Regex> regexes,
|
||||
IReadOnlyList<string> ignoredDownloads
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@@ -37,14 +39,16 @@ public interface IDownloadService : IDisposable
|
||||
/// <param name="categories">The categories by which to filter the downloads.</param>
|
||||
/// <returns>A list of downloads for the provided categories.</returns>
|
||||
Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cleans the downloads.
|
||||
/// </summary>
|
||||
/// <param name="downloads"></param>
|
||||
/// <param name="categoriesToClean">The categories that should be cleaned.</param>
|
||||
/// <param name="excludedHashes">The hashes that should not be cleaned.</param>
|
||||
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes);
|
||||
/// <param name="ignoredDownloads">Downloads to ignore from processing.</param>
|
||||
public abstract Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||
IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a download item.
|
||||
|
||||
@@ -7,6 +7,7 @@ using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Common.Helpers;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Extensions;
|
||||
using Infrastructure.Interceptors;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.Context;
|
||||
@@ -58,18 +59,27 @@ public class QBitService : DownloadService, IQBitService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
StalledResult result = new();
|
||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||
TorrentInfo? download = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (torrent is null)
|
||||
if (download is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(hash);
|
||||
|
||||
if (ignoredDownloads.Count > 0 &&
|
||||
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads)) is true))
|
||||
{
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||
|
||||
if (torrentProperties is null)
|
||||
@@ -83,7 +93,7 @@ public class QBitService : DownloadService, IQBitService
|
||||
&& boolValue;
|
||||
|
||||
// if all files were blocked by qBittorrent
|
||||
if (torrent is { CompletionOn: not null, Downloaded: null or 0 })
|
||||
if (download is { CompletionOn: not null, Downloaded: null or 0 })
|
||||
{
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
@@ -100,7 +110,7 @@ public class QBitService : DownloadService, IQBitService
|
||||
return result;
|
||||
}
|
||||
|
||||
result.ShouldRemove = await IsItemStuckAndShouldRemove(torrent, result.IsPrivate);
|
||||
result.ShouldRemove = await IsItemStuckAndShouldRemove(download, result.IsPrivate);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
@@ -111,23 +121,32 @@ public class QBitService : DownloadService, IQBitService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||
string hash,
|
||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||
BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns,
|
||||
ConcurrentBag<Regex> regexes
|
||||
ConcurrentBag<Regex> regexes,
|
||||
IReadOnlyList<string> ignoredDownloads
|
||||
)
|
||||
{
|
||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||
TorrentInfo? download = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||
.FirstOrDefault();
|
||||
BlockFilesResult result = new();
|
||||
|
||||
if (torrent is null)
|
||||
if (download is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(hash);
|
||||
|
||||
if (ignoredDownloads.Count > 0 &&
|
||||
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads)) is true))
|
||||
{
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
|
||||
|
||||
if (torrentProperties is null)
|
||||
@@ -145,7 +164,7 @@ public class QBitService : DownloadService, IQBitService
|
||||
if (_contentBlockerConfig.IgnorePrivate && isPrivate)
|
||||
{
|
||||
// ignore private trackers
|
||||
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
||||
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -218,7 +237,8 @@ public class QBitService : DownloadService, IQBitService
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||
IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
foreach (TorrentInfo download in downloads)
|
||||
{
|
||||
@@ -227,6 +247,15 @@ public class QBitService : DownloadService, IQBitService
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<TorrentTracker> trackers = await GetTrackersAsync(download.Hash);
|
||||
|
||||
if (ignoredDownloads.Count > 0 &&
|
||||
(download.ShouldIgnore(ignoredDownloads) || trackers.Any(x => x.ShouldIgnore(ignoredDownloads)) is true))
|
||||
{
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
Category? category = categoriesToClean
|
||||
.FirstOrDefault(x => download.Category.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
@@ -303,7 +332,7 @@ public class QBitService : DownloadService, IQBitService
|
||||
{
|
||||
_client.Dispose();
|
||||
}
|
||||
|
||||
|
||||
private async Task<bool> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
|
||||
{
|
||||
if (_queueCleanerConfig.StalledMaxStrikes is 0)
|
||||
@@ -329,4 +358,11 @@ public class QBitService : DownloadService, IQBitService
|
||||
|
||||
return await StrikeAndCheckLimit(torrent.Hash, torrent.Name);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash)
|
||||
{
|
||||
return (await _client.GetTorrentTrackersAsync(hash))
|
||||
.Where(x => !x.Url.ToString().Contains("**"))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Common.Helpers;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Extensions;
|
||||
using Infrastructure.Interceptors;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.Context;
|
||||
@@ -27,6 +28,23 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
private readonly Client _client;
|
||||
private TorrentInfo[]? _torrentsCache;
|
||||
|
||||
private static readonly string[] Fields =
|
||||
[
|
||||
TorrentFields.FILES,
|
||||
TorrentFields.FILE_STATS,
|
||||
TorrentFields.HASH_STRING,
|
||||
TorrentFields.ID,
|
||||
TorrentFields.ETA,
|
||||
TorrentFields.NAME,
|
||||
TorrentFields.STATUS,
|
||||
TorrentFields.IS_PRIVATE,
|
||||
TorrentFields.DOWNLOADED_EVER,
|
||||
TorrentFields.DOWNLOAD_DIR,
|
||||
TorrentFields.SECONDS_SEEDING,
|
||||
TorrentFields.UPLOAD_RATIO,
|
||||
TorrentFields.TRACKERS
|
||||
];
|
||||
|
||||
public TransmissionService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<TransmissionService> logger,
|
||||
@@ -60,21 +78,27 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
StalledResult result = new();
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
TorrentInfo? download = await GetTorrentAsync(hash);
|
||||
|
||||
if (torrent is null)
|
||||
if (download is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool shouldRemove = torrent.FileStats?.Length > 0;
|
||||
result.IsPrivate = torrent.IsPrivate ?? false;
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool shouldRemove = download.FileStats?.Length > 0;
|
||||
result.IsPrivate = download.IsPrivate ?? false;
|
||||
|
||||
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
|
||||
foreach (TransmissionTorrentFileStats? stats in download.FileStats ?? [])
|
||||
{
|
||||
if (!stats.Wanted.HasValue)
|
||||
{
|
||||
@@ -95,7 +119,7 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
}
|
||||
|
||||
// remove if all files are unwanted or download is stuck
|
||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(torrent);
|
||||
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download);
|
||||
|
||||
if (!shouldRemove && result.ShouldRemove)
|
||||
{
|
||||
@@ -106,28 +130,32 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
|
||||
string hash,
|
||||
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash,
|
||||
BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns,
|
||||
ConcurrentBag<Regex> regexes
|
||||
)
|
||||
ConcurrentBag<Regex> regexes, IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
TorrentInfo? download = await GetTorrentAsync(hash);
|
||||
BlockFilesResult result = new();
|
||||
|
||||
if (torrent?.FileStats is null || torrent.Files is null)
|
||||
if (download?.FileStats is null || download.Files is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isPrivate = torrent.IsPrivate ?? false;
|
||||
bool isPrivate = download.IsPrivate ?? false;
|
||||
result.IsPrivate = isPrivate;
|
||||
|
||||
if (_contentBlockerConfig.IgnorePrivate && isPrivate)
|
||||
{
|
||||
// ignore private trackers
|
||||
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
|
||||
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -135,27 +163,27 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
for (int i = 0; i < torrent.Files.Length; i++)
|
||||
for (int i = 0; i < download.Files.Length; i++)
|
||||
{
|
||||
if (torrent.FileStats?[i].Wanted == null)
|
||||
if (download.FileStats?[i].Wanted == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (!torrent.FileStats[i].Wanted.Value)
|
||||
if (!download.FileStats[i].Wanted.Value)
|
||||
{
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_filenameEvaluator.IsValid(torrent.Files[i].Name, blocklistType, patterns, regexes))
|
||||
if (_filenameEvaluator.IsValid(download.Files[i].Name, blocklistType, patterns, regexes))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", torrent.Files[i].Name);
|
||||
_logger.LogInformation("unwanted file found | {file}", download.Files[i].Name);
|
||||
unwantedFiles.Add(i);
|
||||
totalUnwantedFiles++;
|
||||
}
|
||||
@@ -175,7 +203,7 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
|
||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(SetUnwantedFiles, torrent.Id, unwantedFiles.ToArray());
|
||||
await _dryRunInterceptor.InterceptAsync(SetUnwantedFiles, download.Id, unwantedFiles.ToArray());
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -183,22 +211,7 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
/// <inheritdoc/>
|
||||
public override async Task<List<object>?> GetAllDownloadsToBeCleaned(List<Category> categories)
|
||||
{
|
||||
string[] fields = [
|
||||
TorrentFields.FILES,
|
||||
TorrentFields.FILE_STATS,
|
||||
TorrentFields.HASH_STRING,
|
||||
TorrentFields.ID,
|
||||
TorrentFields.ETA,
|
||||
TorrentFields.NAME,
|
||||
TorrentFields.STATUS,
|
||||
TorrentFields.IS_PRIVATE,
|
||||
TorrentFields.DOWNLOADED_EVER,
|
||||
TorrentFields.DOWNLOAD_DIR,
|
||||
TorrentFields.SECONDS_SEEDING,
|
||||
TorrentFields.UPLOAD_RATIO
|
||||
];
|
||||
|
||||
return (await _client.TorrentGetAsync(fields))
|
||||
return (await _client.TorrentGetAsync(Fields))
|
||||
?.Torrents
|
||||
?.Where(x => !string.IsNullOrEmpty(x.HashString))
|
||||
.Where(x => x.Status is 5 or 6)
|
||||
@@ -219,7 +232,8 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes)
|
||||
public override async Task CleanDownloads(List<object> downloads, List<Category> categoriesToClean, HashSet<string> excludedHashes,
|
||||
IReadOnlyList<string> ignoredDownloads)
|
||||
{
|
||||
foreach (TorrentInfo download in downloads)
|
||||
{
|
||||
@@ -227,6 +241,12 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
Category? category = categoriesToClean
|
||||
.FirstOrDefault(x =>
|
||||
@@ -351,20 +371,8 @@ public class TransmissionService : DownloadService, ITransmissionService
|
||||
|
||||
if (_torrentsCache is null || torrent is null)
|
||||
{
|
||||
string[] fields = [
|
||||
TorrentFields.FILES,
|
||||
TorrentFields.FILE_STATS,
|
||||
TorrentFields.HASH_STRING,
|
||||
TorrentFields.ID,
|
||||
TorrentFields.ETA,
|
||||
TorrentFields.NAME,
|
||||
TorrentFields.STATUS,
|
||||
TorrentFields.IS_PRIVATE,
|
||||
TorrentFields.DOWNLOADED_EVER
|
||||
];
|
||||
|
||||
// refresh cache
|
||||
_torrentsCache = (await _client.TorrentGetAsync(fields))
|
||||
_torrentsCache = (await _client.TorrentGetAsync(Fields))
|
||||
?.Torrents;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ namespace Infrastructure.Verticals.Jobs;
|
||||
|
||||
public class JobChainingListener : IJobListener
|
||||
{
|
||||
private readonly string _firstJobName;
|
||||
private readonly string _nextJobName;
|
||||
|
||||
public JobChainingListener(string nextJobName)
|
||||
public JobChainingListener(string firstJobName, string nextJobName)
|
||||
{
|
||||
_firstJobName = firstJobName;
|
||||
_nextJobName = nextJobName;
|
||||
}
|
||||
|
||||
@@ -19,7 +21,7 @@ public class JobChainingListener : IJobListener
|
||||
|
||||
public async Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_nextJobName) || context.JobDetail.Key.Name == _nextJobName)
|
||||
if (string.IsNullOrEmpty(_nextJobName) || context.JobDetail.Key.Name == _nextJobName || context.JobDetail.Key.Name != _firstJobName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ public class NotificationPublisher : INotificationPublisher
|
||||
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
Uri instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
Uri imageUrl = GetImageFromContext(record, instanceType);
|
||||
Uri? imageUrl = GetImageFromContext(record, instanceType);
|
||||
|
||||
ArrNotification notification = new()
|
||||
{
|
||||
@@ -63,42 +63,59 @@ public class NotificationPublisher : INotificationPublisher
|
||||
|
||||
public virtual async Task NotifyQueueItemDeleted(bool removeFromClient, DeleteReason reason)
|
||||
{
|
||||
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
Uri instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
Uri imageUrl = GetImageFromContext(record, instanceType);
|
||||
|
||||
QueueItemDeletedNotification notification = new()
|
||||
try
|
||||
{
|
||||
InstanceType = instanceType,
|
||||
InstanceUrl = instanceUrl,
|
||||
Hash = record.DownloadId.ToLowerInvariant(),
|
||||
Title = $"Deleting item from queue with reason: {reason}",
|
||||
Description = record.Title,
|
||||
Image = imageUrl,
|
||||
Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }]
|
||||
};
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(Notify<QueueItemDeletedNotification>, notification);
|
||||
QueueRecord record = ContextProvider.Get<QueueRecord>(nameof(QueueRecord));
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
Uri instanceUrl = ContextProvider.Get<Uri>(nameof(ArrInstance) + nameof(ArrInstance.Url));
|
||||
Uri? imageUrl = GetImageFromContext(record, instanceType);
|
||||
|
||||
QueueItemDeletedNotification notification = new()
|
||||
{
|
||||
InstanceType = instanceType,
|
||||
InstanceUrl = instanceUrl,
|
||||
Hash = record.DownloadId.ToLowerInvariant(),
|
||||
Title = $"Deleting item from queue with reason: {reason}",
|
||||
Description = record.Title,
|
||||
Image = imageUrl,
|
||||
Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }]
|
||||
};
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(Notify<QueueItemDeletedNotification>, notification);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "failed to notify queue item deleted");
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task NotifyDownloadCleaned(double ratio, TimeSpan seedingTime, string categoryName, CleanReason reason)
|
||||
{
|
||||
DownloadCleanedNotification notification = new()
|
||||
try
|
||||
{
|
||||
Title = $"Cleaned item from download client with reason: {reason}",
|
||||
Description = ContextProvider.Get<string>("downloadName"),
|
||||
Fields =
|
||||
[
|
||||
new() { Title = "Hash", Text = ContextProvider.Get<string>("hash").ToLowerInvariant() },
|
||||
new() { Title = "Category", Text = categoryName.ToLowerInvariant() },
|
||||
new() { Title = "Ratio", Text = $"{ratio.ToString(CultureInfo.InvariantCulture)}%" },
|
||||
new() { Title = "Seeding hours", Text = $"{Math.Round(seedingTime.TotalHours, 0).ToString(CultureInfo.InvariantCulture)}h" }
|
||||
],
|
||||
Level = NotificationLevel.Important
|
||||
};
|
||||
DownloadCleanedNotification notification = new()
|
||||
{
|
||||
Title = $"Cleaned item from download client with reason: {reason}",
|
||||
Description = ContextProvider.Get<string>("downloadName"),
|
||||
Fields =
|
||||
[
|
||||
new() { Title = "Hash", Text = ContextProvider.Get<string>("hash").ToLowerInvariant() },
|
||||
new() { Title = "Category", Text = categoryName.ToLowerInvariant() },
|
||||
new() { Title = "Ratio", Text = $"{ratio.ToString(CultureInfo.InvariantCulture)}%" },
|
||||
new()
|
||||
{
|
||||
Title = "Seeding hours", Text = $"{Math.Round(seedingTime.TotalHours, 0).ToString(CultureInfo.InvariantCulture)}h"
|
||||
}
|
||||
],
|
||||
Level = NotificationLevel.Important
|
||||
};
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(Notify<DownloadCleanedNotification>, notification);
|
||||
await _dryRunInterceptor.InterceptAsync(Notify<DownloadCleanedNotification>, notification);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "failed to notify download cleaned");
|
||||
}
|
||||
}
|
||||
|
||||
[DryRunSafeguard]
|
||||
@@ -107,12 +124,21 @@ public class NotificationPublisher : INotificationPublisher
|
||||
return _messageBus.Publish(message);
|
||||
}
|
||||
|
||||
private static Uri GetImageFromContext(QueueRecord record, InstanceType instanceType) =>
|
||||
instanceType switch
|
||||
private Uri? GetImageFromContext(QueueRecord record, InstanceType instanceType)
|
||||
{
|
||||
Uri? image = instanceType switch
|
||||
{
|
||||
InstanceType.Sonarr => record.Series!.Images.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Radarr => record.Movie!.Images.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Lidarr => record.Album!.Images.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||
InstanceType.Sonarr => record.Series?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Radarr => record.Movie?.Images?.FirstOrDefault(x => x.CoverType == "poster")?.RemoteUrl,
|
||||
InstanceType.Lidarr => record.Album?.Images?.FirstOrDefault(x => x.CoverType == "cover")?.Url,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(instanceType))
|
||||
} ?? throw new Exception("failed to get image url from context");
|
||||
};
|
||||
|
||||
if (image is null)
|
||||
{
|
||||
_logger.LogWarning("no poster found for {title}", record.Title);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Common.Configuration.QueueCleaner;
|
||||
using Domain.Enums;
|
||||
using Domain.Models.Arr;
|
||||
using Domain.Models.Arr.Queue;
|
||||
using Infrastructure.Providers;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.Arr.Interfaces;
|
||||
using Infrastructure.Verticals.Context;
|
||||
@@ -19,7 +20,8 @@ namespace Infrastructure.Verticals.QueueCleaner;
|
||||
public sealed class QueueCleaner : GenericHandler
|
||||
{
|
||||
private readonly QueueCleanerConfig _config;
|
||||
|
||||
private readonly IgnoredDownloadsProvider<QueueCleanerConfig> _ignoredDownloadsProvider;
|
||||
|
||||
public QueueCleaner(
|
||||
ILogger<QueueCleaner> logger,
|
||||
IOptions<QueueCleanerConfig> config,
|
||||
@@ -32,7 +34,8 @@ public sealed class QueueCleaner : GenericHandler
|
||||
LidarrClient lidarrClient,
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
DownloadServiceFactory downloadServiceFactory,
|
||||
INotificationPublisher notifier
|
||||
INotificationPublisher notifier,
|
||||
IgnoredDownloadsProvider<QueueCleanerConfig> ignoredDownloadsProvider
|
||||
) : base(
|
||||
logger, downloadClientConfig,
|
||||
sonarrConfig, radarrConfig, lidarrConfig,
|
||||
@@ -42,10 +45,14 @@ public sealed class QueueCleaner : GenericHandler
|
||||
)
|
||||
{
|
||||
_config = config.Value;
|
||||
_config.Validate();
|
||||
_ignoredDownloadsProvider = ignoredDownloadsProvider;
|
||||
}
|
||||
|
||||
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
{
|
||||
IReadOnlyList<string> ignoredDownloads = await _ignoredDownloadsProvider.GetIgnoredDownloads();
|
||||
|
||||
using var _ = LogContext.PushProperty("InstanceName", instanceType.ToString());
|
||||
|
||||
HashSet<SearchItem> itemsToBeRefreshed = [];
|
||||
@@ -75,15 +82,27 @@ public sealed class QueueCleaner : GenericHandler
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ignoredDownloads.Contains(record.DownloadId, StringComparer.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.LogInformation("skip | {title} | ignored", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
// push record to context
|
||||
ContextProvider.Set(nameof(QueueRecord), record);
|
||||
|
||||
StalledResult stalledCheckResult = new();
|
||||
|
||||
if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None && record.Protocol is "torrent")
|
||||
if (record.Protocol is "torrent")
|
||||
{
|
||||
if (_downloadClientConfig.DownloadClient is Common.Enums.DownloadClient.None)
|
||||
{
|
||||
_logger.LogWarning("skip | download client is not configured | {title}", record.Title);
|
||||
continue;
|
||||
}
|
||||
|
||||
// stalled download check
|
||||
stalledCheckResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
|
||||
stalledCheckResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId, ignoredDownloads);
|
||||
}
|
||||
|
||||
// failed import check
|
||||
|
||||
1
code/test/data/cleanuperr/ignored_downloads
Normal file
1
code/test/data/cleanuperr/ignored_downloads
Normal file
@@ -0,0 +1 @@
|
||||
ignored
|
||||
@@ -175,6 +175,7 @@ services:
|
||||
image: ghcr.io/flmorg/cleanuperr:latest
|
||||
container_name: cleanuperr
|
||||
environment:
|
||||
- TZ=Europe/Bucharest
|
||||
- DRY_RUN=false
|
||||
|
||||
- LOGGING__LOGLEVEL=Debug
|
||||
@@ -190,6 +191,7 @@ services:
|
||||
- TRIGGERS__DOWNLOADCLEANER=0/30 * * * * ?
|
||||
|
||||
- QUEUECLEANER__ENABLED=true
|
||||
- QUEUECLEANER__IGNORED_DOWNLOADS_PATH=/ignored
|
||||
- QUEUECLEANER__RUNSEQUENTIALLY=true
|
||||
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
|
||||
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true
|
||||
@@ -200,10 +202,12 @@ services:
|
||||
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
|
||||
|
||||
- CONTENTBLOCKER__ENABLED=true
|
||||
- CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH=/ignored
|
||||
- CONTENTBLOCKER__IGNORE_PRIVATE=true
|
||||
- CONTENTBLOCKER__DELETE_PRIVATE=false
|
||||
|
||||
- DOWNLOADCLEANER__ENABLED=true
|
||||
- DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH=/ignored
|
||||
- DOWNLOADCLEANER__DELETE_PRIVATE=false
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__NAME=tv-sonarr
|
||||
- DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO=-1
|
||||
@@ -255,6 +259,7 @@ services:
|
||||
# - NOTIFIARR__CHANNEL_ID=discord_channel_id
|
||||
volumes:
|
||||
- ./data/cleanuperr/logs:/var/logs
|
||||
- ./data/cleanuperr/ignored_downloads:/ignored
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- qbittorrent
|
||||
|
||||
452
variables.md
452
variables.md
@@ -1,45 +1,52 @@
|
||||
## Table of contents
|
||||
- [General settings](variables.md#general-settings)
|
||||
- [Queue Cleaner settings](variables.md#queue-cleaner-settings)
|
||||
- [Content Blocker settings](variables.md#content-blocker-settings)
|
||||
- [Download Cleaner settings](variables.md#download-cleaner-settings)
|
||||
- [Download Client settings](variables.md#download-client-settings)
|
||||
- [Arr settings](variables.md#arr-settings)
|
||||
- [Notification settings](variables.md#notification-settings)
|
||||
- [Advanced settings](variables.md#advanced-settings)
|
||||
- [General settings](#general-settings)
|
||||
- [Queue Cleaner settings](#queue-cleaner-settings)
|
||||
- [Content Blocker settings](#content-blocker-settings)
|
||||
- [Download Cleaner settings](#download-cleaner-settings)
|
||||
- [Download Client settings](#download-client-settings)
|
||||
- [Arr settings](#arr-settings)
|
||||
- [Notification settings](#notification-settings)
|
||||
- [Advanced settings](#advanced-settings)
|
||||
|
||||
#
|
||||
|
||||
### General settings
|
||||
|
||||
**`DRY_RUN`**
|
||||
#### **`TZ`**
|
||||
- The time zone to use.
|
||||
- Type: String.
|
||||
- Possible values: Any valid timezone.
|
||||
- Default: `UTC`.
|
||||
- Required: No.
|
||||
|
||||
#### **`DRY_RUN`**
|
||||
- When enabled, simulates irreversible operations (like deletions and notifications) without making actual changes.
|
||||
- Type: Boolean.
|
||||
- Possible values: `true`, `false`.
|
||||
- Default: `false`.
|
||||
- Required: No.
|
||||
|
||||
**`LOGGING__LOGLEVEL`**
|
||||
#### **`LOGGING__LOGLEVEL`**
|
||||
- Controls the detail level of application logs.
|
||||
- Type: String.
|
||||
- Possible values: `Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal`.
|
||||
- Default: `Information`.
|
||||
- Required: No.
|
||||
|
||||
**`LOGGING__FILE__ENABLED`**
|
||||
#### **`LOGGING__FILE__ENABLED`**
|
||||
- Enables logging to a file.
|
||||
- Type: Boolean.
|
||||
- Possible values: `true`, `false`.
|
||||
- Default: `false`.
|
||||
- Required: No.
|
||||
|
||||
**`LOGGING__FILE__PATH`**
|
||||
#### **`LOGGING__FILE__PATH`**
|
||||
- Directory where log files will be saved.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`LOGGING__ENHANCED`**
|
||||
#### **`LOGGING__ENHANCED`**
|
||||
- Provides more detailed descriptions in logs whenever possible.
|
||||
- Type: Boolean.
|
||||
- Possible values: `true`, `false`.
|
||||
@@ -51,7 +58,7 @@
|
||||
|
||||
### Queue Cleaner settings
|
||||
|
||||
**`TRIGGERS__QUEUECLEANER`**
|
||||
#### **`TRIGGERS__QUEUECLEANER`**
|
||||
- Cron schedule for the queue cleaner job.
|
||||
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
||||
- Default: `0 0/5 * * * ?` (every 5 minutes).
|
||||
@@ -59,9 +66,9 @@
|
||||
|
||||
> [!NOTE]
|
||||
> - Maximum interval is 6 hours.
|
||||
> - Ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`.
|
||||
> - Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`.
|
||||
|
||||
**`QUEUECLEANER__ENABLED`**
|
||||
#### **`QUEUECLEANER__ENABLED`**
|
||||
- Enables or disables the queue cleaning functionality.
|
||||
- When enabled, processes all items in the *arr queue.
|
||||
- Type: Boolean
|
||||
@@ -69,7 +76,32 @@
|
||||
- Default: `true`
|
||||
- Required: No.
|
||||
|
||||
**`QUEUECLEANER__RUNSEQUENTIALLY`**
|
||||
#### **`QUEUECLEANER__IGNORED_DOWNLOADS_PATH`**
|
||||
- Local path to the file containing ignored downloads.
|
||||
- If the contents of the file are changed, they will be reloaded on the next job run.
|
||||
- Accepted values:
|
||||
- torrent hash
|
||||
- qBitTorrent tag or category
|
||||
- Deluge label
|
||||
- Transmission category (last directory from the save location)
|
||||
- torrent tracker domain
|
||||
- Each value needs to be on a new line.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
- Example: `/ignored.txt`.
|
||||
- Example of file contents:
|
||||
```
|
||||
fa800a7d7c443a2c3561d1f8f393c089036dade1
|
||||
tv-sonarr
|
||||
qbit-tag
|
||||
mytracker.com
|
||||
...
|
||||
```
|
||||
>[!IMPORTANT]
|
||||
> Some people have experienced problems using Docker where the mounted file would not update inside the container if it was modified on the host. This is a Docker configuration problem and can not be solved by cleanuperr.
|
||||
|
||||
#### **`QUEUECLEANER__RUNSEQUENTIALLY`**
|
||||
- Controls whether queue cleaner runs after content blocker instead of in parallel.
|
||||
- When `true`, streamlines the cleaning process by running immediately after content blocker.
|
||||
- Type: Boolean
|
||||
@@ -77,23 +109,24 @@
|
||||
- Default: `true`
|
||||
- Required: No.
|
||||
|
||||
**`QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES`**
|
||||
#### **`QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES`**
|
||||
- Number of strikes before removing a failed import.
|
||||
- Set to `0` to never remove failed imports.
|
||||
- A strike is given when an item is stalled, stuck in metadata downloading, or failed to be imported.
|
||||
- A strike is given when an item fails to be imported.
|
||||
- Type: Integer
|
||||
- Possible values: `0` or greater
|
||||
- Default: `0`
|
||||
- Required: No.
|
||||
> [!NOTE]
|
||||
> If not set to `0`, the minimum value is `3`.
|
||||
|
||||
**`QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE`**
|
||||
#### **`QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE`**
|
||||
- Controls whether to ignore failed imports from private trackers.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE`**
|
||||
#### **`QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE`**
|
||||
- Controls whether to delete failed imports from private trackers from the download client.
|
||||
- Has no effect if `QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE` is `true`.
|
||||
- Type: Boolean
|
||||
@@ -104,42 +137,43 @@
|
||||
> [!WARNING]
|
||||
> Setting `QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
||||
|
||||
**`QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS`**
|
||||
#### **`QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS`**
|
||||
- Patterns to look for in failed import messages that should be ignored.
|
||||
- Multiple patterns can be specified using incrementing numbers starting from 0.
|
||||
- Type: String array
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
- Example:
|
||||
```yaml
|
||||
QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0: "title mismatch"
|
||||
QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
```
|
||||
```yaml
|
||||
QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0: "title mismatch"
|
||||
QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
```
|
||||
|
||||
**`QUEUECLEANER__STALLED_MAX_STRIKES`**
|
||||
#### **`QUEUECLEANER__STALLED_MAX_STRIKES`**
|
||||
- Number of strikes before removing a stalled download.
|
||||
- Set to `0` to never remove stalled downloads.
|
||||
- A strike is given when download speed is 0.
|
||||
- A strike is given when an item is stalled (not downloading) or stuck while downloading metadata.
|
||||
- Type: Integer
|
||||
- Possible values: `0` or greater
|
||||
- Default: `0`
|
||||
- Required: No.
|
||||
> [!NOTE]
|
||||
> If not set to `0`, the minimum value is `3`.
|
||||
|
||||
**`QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS`**
|
||||
#### **`QUEUECLEANER__STALLED_RESET_STRIKES_ON_PROGRESS`**
|
||||
- Controls whether to remove strikes if any download progress was made since last checked.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`QUEUECLEANER__STALLED_IGNORE_PRIVATE`**
|
||||
#### **`QUEUECLEANER__STALLED_IGNORE_PRIVATE`**
|
||||
- Controls whether to ignore stalled downloads from private trackers.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`QUEUECLEANER__STALLED_DELETE_PRIVATE`**
|
||||
#### **`QUEUECLEANER__STALLED_DELETE_PRIVATE`**
|
||||
- Controls whether to delete stalled private downloads from the download client.
|
||||
- Has no effect if `QUEUECLEANER__STALLED_IGNORE_PRIVATE` is `true`.
|
||||
- Type: Boolean
|
||||
@@ -154,7 +188,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
|
||||
### Content Blocker settings
|
||||
|
||||
**`TRIGGERS__CONTENTBLOCKER`**
|
||||
#### **`TRIGGERS__CONTENTBLOCKER`**
|
||||
- Cron schedule for the content blocker job.
|
||||
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
||||
- Default: `0 0/5 * * * ?` (every 5 minutes).
|
||||
@@ -163,7 +197,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
> [!NOTE]
|
||||
> - Maximum interval is 6 hours.
|
||||
|
||||
**`CONTENTBLOCKER__ENABLED`**
|
||||
#### **`CONTENTBLOCKER__ENABLED`**
|
||||
- Enables or disables the content blocker functionality.
|
||||
- When enabled, processes all items in the *arr queue and marks unwanted files.
|
||||
- Type: Boolean
|
||||
@@ -171,14 +205,39 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`CONTENTBLOCKER__IGNORE_PRIVATE`**
|
||||
#### **`CONTENTBLOCKER__IGNORED_DOWNLOADS_PATH`**
|
||||
- Local path to the file containing ignored downloads.
|
||||
- If the contents of the file are changed, they will be reloaded on the next job run.
|
||||
- Accepted values:
|
||||
- torrent hash
|
||||
- qBitTorrent tag or category
|
||||
- Deluge label
|
||||
- Transmission category (last directory from the save location)
|
||||
- torrent tracker domain
|
||||
- Each value needs to be on a new line.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
- Example: `/ignored.txt`.
|
||||
- Example of file contents:
|
||||
```
|
||||
fa800a7d7c443a2c3561d1f8f393c089036dade1
|
||||
tv-sonarr
|
||||
qbit-tag
|
||||
mytracker.com
|
||||
...
|
||||
```
|
||||
>[!IMPORTANT]
|
||||
> Some people have experienced problems using Docker where the mounted file would not update inside the container if it was modified on the host. This is a Docker configuration problem and can not be solved by cleanuperr.
|
||||
|
||||
#### **`CONTENTBLOCKER__IGNORE_PRIVATE`**
|
||||
- Controls whether to ignore downloads from private trackers.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`CONTENTBLOCKER__DELETE_PRIVATE`**
|
||||
#### **`CONTENTBLOCKER__DELETE_PRIVATE`**
|
||||
- Controls whether to delete private downloads that have all files blocked from the download client.
|
||||
- Has no effect if `CONTENTBLOCKER__IGNORE_PRIVATE` is `true`.
|
||||
- Type: Boolean
|
||||
@@ -193,7 +252,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
|
||||
### Download Cleaner settings
|
||||
|
||||
**`TRIGGERS__DOWNLOADCLEANER`**
|
||||
#### **`TRIGGERS__DOWNLOADCLEANER`**
|
||||
- Cron schedule for the download cleaner job.
|
||||
- Type: String - [Quartz cron format](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
|
||||
- Default: `0 0 * * * ?` (every hour).
|
||||
@@ -202,7 +261,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
> [!NOTE]
|
||||
> - Maximum interval is 6 hours.
|
||||
|
||||
**`DOWNLOADCLEANER__ENABLED`**
|
||||
#### **`DOWNLOADCLEANER__ENABLED`**
|
||||
- Enables or disables the download cleaner functionality.
|
||||
- When enabled, automatically cleans up downloads that have been seeding for a certain amount of time.
|
||||
- Type: Boolean.
|
||||
@@ -210,7 +269,32 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`DOWNLOADCLEANER__DELETE_PRIVATE`**
|
||||
#### **`DOWNLOADCLEANER__IGNORED_DOWNLOADS_PATH`**
|
||||
- Local path to the file containing ignored downloads.
|
||||
- If the contents of the file are changed, they will be reloaded on the next job run.
|
||||
- Accepted values:
|
||||
- torrent hash
|
||||
- qBitTorrent tag or category
|
||||
- Deluge label
|
||||
- Transmission category (last directory from the save location)
|
||||
- torrent tracker domain
|
||||
- Each value needs to be on a new line.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
- Example: `/ignored.txt`.
|
||||
- Example of file contents:
|
||||
```
|
||||
fa800a7d7c443a2c3561d1f8f393c089036dade1
|
||||
tv-sonarr
|
||||
qbit-tag
|
||||
mytracker.com
|
||||
...
|
||||
```
|
||||
>[!IMPORTANT]
|
||||
> Some people have experienced problems using Docker where the mounted file would not update inside the container if it was modified on the host. This is a Docker configuration problem and can not be solved by cleanuperr.
|
||||
|
||||
#### **`DOWNLOADCLEANER__DELETE_PRIVATE`**
|
||||
- Controls whether to delete private downloads.
|
||||
- Type: Boolean.
|
||||
- Possible values: `true`, `false`
|
||||
@@ -220,7 +304,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
> [!WARNING]
|
||||
> Setting `DOWNLOADCLEANER__DELETE_PRIVATE=true` means you don't care about seeding, ratio, H&R and potentially losing your tracker account.
|
||||
|
||||
**`DOWNLOADCLEANER__CATEGORIES__0__NAME`**
|
||||
#### **`DOWNLOADCLEANER__CATEGORIES__0__NAME`**
|
||||
- Name of the category to clean.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
@@ -228,18 +312,21 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
|
||||
> [!NOTE]
|
||||
> 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 name of the download location.
|
||||
>
|
||||
> For Transmission, the category name is the last directory from the save location.
|
||||
|
||||
**`DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO`**
|
||||
#### **`DOWNLOADCLEANER__CATEGORIES__0__MAX_RATIO`**
|
||||
- Maximum ratio to reach before removing a download.
|
||||
- Type: Decimal.
|
||||
- Possible values: `-1` or greater (`-1` means no limit or disabled).
|
||||
- Default: `-1`
|
||||
- Required: No.
|
||||
|
||||
**`DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME`**
|
||||
#### **`DOWNLOADCLEANER__CATEGORIES__0__MIN_SEED_TIME`**
|
||||
- Minimum number of hours to seed before removing a download, if the ratio has been met.
|
||||
- Used with `MAX_RATIO` to ensure a minimum seed time.
|
||||
- Type: Decimal.
|
||||
@@ -247,7 +334,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
- Default: `0`
|
||||
- Required: No.
|
||||
|
||||
**`DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME`**
|
||||
#### **`DOWNLOADCLEANER__CATEGORIES__0__MAX_SEED_TIME`**
|
||||
- Maximum number of hours to seed before removing a download.
|
||||
- Type: Decimal.
|
||||
- Possible values: `-1` or greater (`-1` means no limit or disabled).
|
||||
@@ -270,7 +357,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
|
||||
### Download Client settings
|
||||
|
||||
**`DOWNLOAD_CLIENT`**
|
||||
#### **`DOWNLOAD_CLIENT`**
|
||||
- Specifies which download client is used by *arrs.
|
||||
- Type: String.
|
||||
- Possible values: `none`, `qbittorrent`, `deluge`, `transmission`.
|
||||
@@ -280,49 +367,49 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
> [!NOTE]
|
||||
> Only one download client can be enabled at a time. If you have more than one download client, you should deploy multiple instances of cleanuperr.
|
||||
|
||||
**`QBITTORRENT__URL`**
|
||||
#### **`QBITTORRENT__URL`**
|
||||
- URL of the qBittorrent instance.
|
||||
- Type: String.
|
||||
- Default: `http://localhost:8080`.
|
||||
- Required: No.
|
||||
|
||||
**`QBITTORRENT__USERNAME`**
|
||||
#### **`QBITTORRENT__USERNAME`**
|
||||
- Username for qBittorrent authentication.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`QBITTORRENT__PASSWORD`**
|
||||
#### **`QBITTORRENT__PASSWORD`**
|
||||
- Password for qBittorrent authentication.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`DELUGE__URL`**
|
||||
#### **`DELUGE__URL`**
|
||||
- URL of the Deluge instance.
|
||||
- Type: String.
|
||||
- Default: `http://localhost:8112`.
|
||||
- Required: No.
|
||||
|
||||
**`DELUGE__PASSWORD`**
|
||||
#### **`DELUGE__PASSWORD`**
|
||||
- Password for Deluge authentication.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`TRANSMISSION__URL`**
|
||||
#### **`TRANSMISSION__URL`**
|
||||
- URL of the Transmission instance.
|
||||
- Type: String.
|
||||
- Default: `http://localhost:9091`.
|
||||
- Required: No.
|
||||
|
||||
**`TRANSMISSION__USERNAME`**
|
||||
#### **`TRANSMISSION__USERNAME`**
|
||||
- Username for Transmission authentication.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`TRANSMISSION__PASSWORD`**
|
||||
#### **`TRANSMISSION__PASSWORD`**
|
||||
- Password for Transmission authentication.
|
||||
- Type: String.
|
||||
- Default: Empty.
|
||||
@@ -332,112 +419,6 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
|
||||
### Arr settings
|
||||
|
||||
**`SONARR__ENABLED`**
|
||||
- Enables or disables Sonarr cleanup.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`SONARR__BLOCK__TYPE`**
|
||||
- Determines how file blocking works for Sonarr.
|
||||
- Type: String
|
||||
- Possible values: `blacklist`, `whitelist`
|
||||
- Default: `blacklist`
|
||||
- Required: No.
|
||||
|
||||
**`SONARR__BLOCK__PATH`**
|
||||
- Path to the blocklist file (local file or URL).
|
||||
- Must be JSON compatible.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`SONARR__SEARCHTYPE`**
|
||||
- Determines what to search for after removing a queue item.
|
||||
- Type: String
|
||||
- Possible values: `Episode`, `Season`, `Series`
|
||||
- Default: `Episode`
|
||||
- Required: No.
|
||||
|
||||
**`SONARR__INSTANCES__0__URL`**
|
||||
- URL of the Sonarr instance.
|
||||
- Type: String
|
||||
- Default: `http://localhost:8989`
|
||||
- Required: No.
|
||||
|
||||
**`SONARR__INSTANCES__0__APIKEY`**
|
||||
- API key for the Sonarr instance.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`RADARR__ENABLED`**
|
||||
- Enables or disables Radarr cleanup.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`RADARR__BLOCK__TYPE`**
|
||||
- Determines how file blocking works for Radarr.
|
||||
- Type: String
|
||||
- Possible values: `blacklist`, `whitelist`
|
||||
- Default: `blacklist`
|
||||
- Required: No.
|
||||
|
||||
**`RADARR__BLOCK__PATH`**
|
||||
- Path to the blocklist file (local file or URL).
|
||||
- Must be JSON compatible.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`RADARR__INSTANCES__0__URL`**
|
||||
- URL of the Radarr instance.
|
||||
- Type: String
|
||||
- Default: `http://localhost:7878`
|
||||
- Required: No.
|
||||
|
||||
**`RADARR__INSTANCES__0__APIKEY`**
|
||||
- API key for the Radarr instance.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`LIDARR__ENABLED`**
|
||||
- Enables or disables Lidarr cleanup.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`LIDARR__BLOCK__TYPE`**
|
||||
- Determines how file blocking works for Lidarr.
|
||||
- Type: String
|
||||
- Possible values: `blacklist`, `whitelist`
|
||||
- Default: `blacklist`
|
||||
- Required: No.
|
||||
|
||||
**`LIDARR__BLOCK__PATH`**
|
||||
- Path to the blocklist file (local file or URL).
|
||||
- Must be JSON compatible.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`LIDARR__INSTANCES__0__URL`**
|
||||
- URL of the Lidarr instance.
|
||||
- Type: String
|
||||
- Default: `http://localhost:8686`
|
||||
- Required: No.
|
||||
|
||||
**`LIDARR__INSTANCES__0__APIKEY`**
|
||||
- API key for the Lidarr instance.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
> [!NOTE]
|
||||
> Multiple instances can be specified for each *arr using this format, where `<NUMBER>` starts from 0:
|
||||
> ```yaml
|
||||
@@ -445,8 +426,29 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
> <ARR>__INSTANCES__<NUMBER>__APIKEY
|
||||
> ```
|
||||
|
||||
#### **`SONARR__ENABLED`**
|
||||
- Enables or disables Sonarr cleanup.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`SONARR__BLOCK__TYPE`**
|
||||
- Determines how file blocking works for Sonarr.
|
||||
- Type: String
|
||||
- Possible values: `blacklist`, `whitelist`
|
||||
- Default: `blacklist`
|
||||
- Required: No.
|
||||
|
||||
#### **`SONARR__BLOCK__PATH`**
|
||||
- Path to the blocklist file (local file or URL).
|
||||
- Must be JSON compatible.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
> [!NOTE]
|
||||
> The blocklists (blacklist/whitelist) support the following patterns:
|
||||
> The blocklists support the following patterns:
|
||||
> ```
|
||||
> *example // file name ends with "example"
|
||||
> example* // file name starts with "example"
|
||||
@@ -456,47 +458,155 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
> ```
|
||||
|
||||
> [!NOTE]
|
||||
> [This blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist) and [this whitelist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist) can be used for Sonarr and Radarr, but they are not suitable for other *arrs.
|
||||
> [This blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist), [this permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive) and [this whitelist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist) can be used for Sonarr and Radarr.
|
||||
|
||||
#### **`SONARR__SEARCHTYPE`**
|
||||
- Determines what to search for after removing a queue item.
|
||||
- Type: String
|
||||
- Possible values: `Episode`, `Season`, `Series`
|
||||
- Default: `Episode`
|
||||
- Required: No.
|
||||
|
||||
#### **`SONARR__INSTANCES__0__URL`**
|
||||
- URL of the Sonarr instance.
|
||||
- Type: String
|
||||
- Default: `http://localhost:8989`
|
||||
- Required: No.
|
||||
|
||||
#### **`SONARR__INSTANCES__0__APIKEY`**
|
||||
- API key for the Sonarr instance.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
#### **`RADARR__ENABLED`**
|
||||
- Enables or disables Radarr cleanup.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`RADARR__BLOCK__TYPE`**
|
||||
- Determines how file blocking works for Radarr.
|
||||
- Type: String
|
||||
- Possible values: `blacklist`, `whitelist`
|
||||
- Default: `blacklist`
|
||||
- Required: No.
|
||||
|
||||
#### **`RADARR__BLOCK__PATH`**
|
||||
- Path to the blocklist file (local file or URL).
|
||||
- Must be JSON compatible.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
> [!NOTE]
|
||||
> The blocklists support the following patterns:
|
||||
> ```
|
||||
> *example // file name ends with "example"
|
||||
> example* // file name starts with "example"
|
||||
> *example* // file name has "example" in the name
|
||||
> example // file name is exactly the word "example"
|
||||
> regex:<ANY_REGEX> // regex that needs to be marked at the start of the line with "regex:"
|
||||
> ```
|
||||
|
||||
> [!NOTE]
|
||||
> [This blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist), [this permissive blacklist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/blacklist_permissive) and [this whitelist](https://raw.githubusercontent.com/flmorg/cleanuperr/refs/heads/main/whitelist) can be used for Sonarr and Radarr.
|
||||
|
||||
#### **`RADARR__INSTANCES__0__URL`**
|
||||
- URL of the Radarr instance.
|
||||
- Type: String
|
||||
- Default: `http://localhost:7878`
|
||||
- Required: No.
|
||||
|
||||
#### **`RADARR__INSTANCES__0__APIKEY`**
|
||||
- API key for the Radarr instance.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
#### **`LIDARR__ENABLED`**
|
||||
- Enables or disables Lidarr cleanup.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
#### **`LIDARR__BLOCK__TYPE`**
|
||||
- Determines how file blocking works for Lidarr.
|
||||
- Type: String
|
||||
- Possible values: `blacklist`, `whitelist`
|
||||
- Default: `blacklist`
|
||||
- Required: No.
|
||||
|
||||
#### **`LIDARR__BLOCK__PATH`**
|
||||
- Path to the blocklist file (local file or URL).
|
||||
- Must be JSON compatible.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
> [!NOTE]
|
||||
> The blocklists support the following patterns:
|
||||
> ```
|
||||
> *example // file name ends with "example"
|
||||
> example* // file name starts with "example"
|
||||
> *example* // file name has "example" in the name
|
||||
> example // file name is exactly the word "example"
|
||||
> regex:<ANY_REGEX> // regex that needs to be marked at the start of the line with "regex:"
|
||||
> ```
|
||||
|
||||
#### **`LIDARR__INSTANCES__0__URL`**
|
||||
- URL of the Lidarr instance.
|
||||
- Type: String
|
||||
- Default: `http://localhost:8686`
|
||||
- Required: No.
|
||||
|
||||
#### **`LIDARR__INSTANCES__0__APIKEY`**
|
||||
- API key for the Lidarr instance.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
#
|
||||
|
||||
### Notification settings
|
||||
|
||||
**`NOTIFIARR__API_KEY`**
|
||||
#### **`NOTIFIARR__API_KEY`**
|
||||
- Notifiarr API key for sending notifications.
|
||||
- Requires Notifiarr's [`Passthrough`](https://notifiarr.wiki/en/Website/Integrations/Passthrough) integration to work.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`NOTIFIARR__CHANNEL_ID`**
|
||||
#### **`NOTIFIARR__CHANNEL_ID`**
|
||||
- Discord channel ID where notifications will be sent.
|
||||
- Type: String
|
||||
- Default: Empty.
|
||||
- Required: No.
|
||||
|
||||
**`NOTIFIARR__ON_IMPORT_FAILED_STRIKE`**
|
||||
#### **`NOTIFIARR__ON_IMPORT_FAILED_STRIKE`**
|
||||
- Controls whether to notify when an item receives a failed import strike.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`NOTIFIARR__ON_STALLED_STRIKE`**
|
||||
#### **`NOTIFIARR__ON_STALLED_STRIKE`**
|
||||
- Controls whether to notify when an item receives a stalled download strike.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`NOTIFIARR__ON_QUEUE_ITEM_DELETED`**
|
||||
#### **`NOTIFIARR__ON_QUEUE_ITEM_DELETED`**
|
||||
- Controls whether to notify when a queue item is deleted.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
- Default: `false`
|
||||
- Required: No.
|
||||
|
||||
**`NOTIFIARR__ON_DOWNLOAD_CLEANED`**
|
||||
#### **`NOTIFIARR__ON_DOWNLOAD_CLEANED`**
|
||||
- Controls whether to notify when a download is cleaned.
|
||||
- Type: Boolean
|
||||
- Possible values: `true`, `false`
|
||||
@@ -507,7 +617,7 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
|
||||
### Advanced settings
|
||||
|
||||
**`HTTP_MAX_RETRIES`**
|
||||
#### **`HTTP_MAX_RETRIES`**
|
||||
- The number of times to retry a failed HTTP call.
|
||||
- Applies to calls to *arrs, download clients, and other services.
|
||||
- Type: Integer
|
||||
@@ -515,10 +625,10 @@ QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1: "manual import required"
|
||||
- Default: `0`
|
||||
- Required: No.
|
||||
|
||||
**`HTTP_TIMEOUT`**
|
||||
#### **`HTTP_TIMEOUT`**
|
||||
- The number of seconds to wait before failing an HTTP call.
|
||||
- Applies to calls to *arrs, download clients, and other services.
|
||||
- Type: Integer
|
||||
- Possible values: Greater than `0`
|
||||
- Possible values: Greater than `0`.
|
||||
- Default: `100`
|
||||
- Required: No.
|
||||
Reference in New Issue
Block a user