diff --git a/README.md b/README.md
index 7b4e1efa..0ac00a36 100644
--- a/README.md
+++ b/README.md
@@ -106,13 +106,16 @@ services:
- QUEUECLEANER__RUNSEQUENTIALLY=true
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false
+ - QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=title mismatch
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required
- QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
+ - QUEUECLEANER__STALLED_DELETE_PRIVATE=false
- CONTENTBLOCKER__ENABLED=true
- - CONTENTBLOCKER__IGNORE_PRIVATE=true
+ - CONTENTBLOCKER__IGNORE_PRIVATE=false
+ - CONTENTBLOCKER__DELETE_PRIVATE=false
- DOWNLOAD_CLIENT=none
# OR
@@ -166,24 +169,27 @@ services:
| Variable | Required | Description | Default value |
|---|---|---|---|
-| LOGGING__LOGLEVEL | No | Can be `Verbose`, `Debug`, `Information`, `Warning`, `Error` or `Fatal` | `Information` |
-| LOGGING__FILE__ENABLED | No | Enable or disable logging to file | false |
-| LOGGING__FILE__PATH | No | Directory where to save the log files | empty |
-| LOGGING__ENHANCED | No | Enhance logs whenever possible
A more detailed description is provided [here](variables.md#LOGGING__ENHANCED) | true |
+| LOGGING__LOGLEVEL | No | Can be `Verbose`, `Debug`, `Information`, `Warning`, `Error` or `Fatal`. | `Information` |
+| LOGGING__FILE__ENABLED | No | Enable or disable logging to file. | false |
+| LOGGING__FILE__PATH | No | Directory where to save the log files. | empty |
+| LOGGING__ENHANCED | No | Enhance logs whenever possible.
A more detailed description is provided. [here](variables.md#LOGGING__ENHANCED) | true |
|||||
-| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)
Can be a max of 6h interval
**Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`** | 0 0/5 * * * ? |
-| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)
Can be a max of 6h interval | 0 0/5 * * * ? |
+| TRIGGERS__QUEUECLEANER | Yes if queue cleaner is enabled | - [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
- Can be a max of 6h interval.
- **Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`**. | 0 0/5 * * * ? |
+| TRIGGERS__CONTENTBLOCKER | Yes if content blocker is enabled | - [Quartz cron trigger](https://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html).
- Can be a max of 6h interval. | 0 0/5 * * * ? |
|||||
-| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true |
-| QUEUECLEANER__RUNSEQUENTIALLY | No | If set to true, the queue cleaner will run after the content blocker instead of running in parallel, streamlining the cleaning process | true |
-| QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES | No | After how many strikes should a failed import be removed
0 means never | 0 |
-| QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE | No | Whether to ignore failed imports from private trackers | false |
-| QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0 | No | First pattern to look for when an import is failed
If the specified message pattern is found, the item is skipped | empty |
-| QUEUECLEANER__STALLED_MAX_STRIKES | No | After how many strikes should a stalled download be removed
0 means never | 0 |
-| QUEUECLEANER__STALLED_IGNORE_PRIVATE | No | Whether to ignore stalled downloads from private trackers | false |
+| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner. | true |
+| QUEUECLEANER__RUNSEQUENTIALLY | No | If set to true, the queue cleaner will run after the content blocker instead of running in parallel, streamlining the cleaning process. | true |
+| QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES | No | - After how many strikes should a failed import be removed.
- 0 means never. | 0 |
+| QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE | No | Whether to ignore failed imports from private trackers. | false |
+| QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE | No | - Whether to delete failed imports from the download client.
- Does not have any effect if `QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE` is `true`.
- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
+| QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0 | No | - First pattern to look for when an import is failed.
- If the specified message pattern is found, the item is skipped. | empty |
+| QUEUECLEANER__STALLED_MAX_STRIKES | No | - After how many strikes should a stalled download be removed.
- 0 means never. | 0 |
+| QUEUECLEANER__STALLED_IGNORE_PRIVATE | No | Whether to ignore stalled downloads from private trackers. | false |
+| QUEUECLEANER__STALLED_DELETE_PRIVATE | No | - Whether to delete stalled downloads from the download client.
- Does not have any effect if `QUEUECLEANER__STALLED_IGNORE_PRIVATE` is `true`.
- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
|||||
-| CONTENTBLOCKER__ENABLED | No | Enable or disable the content blocker | false |
-| CONTENTBLOCKER__IGNORE_PRIVATE | No | Whether to ignore downloads from private trackers | false |
+| CONTENTBLOCKER__ENABLED | No | Enable or disable the content blocker. | false |
+| CONTENTBLOCKER__IGNORE_PRIVATE | No | Whether to ignore downloads from private trackers. | false |
+| CONTENTBLOCKER__DELETE_PRIVATE | No | - Whether to delete items that have all files blocked from the download client.
- Does not have any effect if `CONTENTBLOCKER__IGNORE_PRIVATE` is `true`.
- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
### Download client variables
diff --git a/code/Common/Configuration/ContentBlocker/ContentBlockerConfig.cs b/code/Common/Configuration/ContentBlocker/ContentBlockerConfig.cs
index fd882340..a0fb4b43 100644
--- a/code/Common/Configuration/ContentBlocker/ContentBlockerConfig.cs
+++ b/code/Common/Configuration/ContentBlocker/ContentBlockerConfig.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration;
namespace Common.Configuration.ContentBlocker;
@@ -11,6 +11,9 @@ public sealed record ContentBlockerConfig : IJobConfig
[ConfigurationKeyName("IGNORE_PRIVATE")]
public bool IgnorePrivate { get; init; }
+ [ConfigurationKeyName("DELETE_PRIVATE")]
+ public bool DeletePrivate { get; init; }
+
public void Validate()
{
}
diff --git a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs b/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs
index b6fa5bea..191b5d98 100644
--- a/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs
+++ b/code/Common/Configuration/QueueCleaner/QueueCleanerConfig.cs
@@ -16,6 +16,9 @@ public sealed record QueueCleanerConfig : IJobConfig
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PRIVATE")]
public bool ImportFailedIgnorePrivate { get; init; }
+ [ConfigurationKeyName("IMPORT_FAILED_DELETE_PRIVATE")]
+ public bool ImportFailedDeletePrivate { get; init; }
+
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PATTERNS")]
public List? ImportFailedIgnorePatterns { get; init; }
@@ -24,6 +27,9 @@ public sealed record QueueCleanerConfig : IJobConfig
[ConfigurationKeyName("STALLED_IGNORE_PRIVATE")]
public bool StalledIgnorePrivate { get; init; }
+
+ [ConfigurationKeyName("STALLED_DELETE_PRIVATE")]
+ public bool StalledDeletePrivate { get; init; }
public void Validate()
{
diff --git a/code/Executable/appsettings.Development.json b/code/Executable/appsettings.Development.json
index fe798844..d9800e66 100644
--- a/code/Executable/appsettings.Development.json
+++ b/code/Executable/appsettings.Development.json
@@ -15,18 +15,21 @@
},
"ContentBlocker": {
"Enabled": true,
- "IGNORE_PRIVATE": true
+ "IGNORE_PRIVATE": true,
+ "DELETE_PRIVATE": false
},
"QueueCleaner": {
"Enabled": true,
"RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5,
"IMPORT_FAILED_IGNORE_PRIVATE": true,
+ "IMPORT_FAILED_DELETE_PRIVATE": false,
"IMPORT_FAILED_IGNORE_PATTERNS": [
"file is a sample"
],
"STALLED_MAX_STRIKES": 5,
- "STALLED_IGNORE_PRIVATE": true
+ "STALLED_IGNORE_PRIVATE": true,
+ "STALLED_DELETE_PRIVATE": false
},
"DOWNLOAD_CLIENT": "qbittorrent",
"qBittorrent": {
diff --git a/code/Executable/appsettings.json b/code/Executable/appsettings.json
index e30f3753..3a43a236 100644
--- a/code/Executable/appsettings.json
+++ b/code/Executable/appsettings.json
@@ -22,9 +22,11 @@
"RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5,
"IMPORT_FAILED_IGNORE_PRIVATE": false,
+ "IMPORT_FAILED_DELETE_PRIVATE": false,
"IMPORT_FAILED_IGNORE_PATTERNS": [],
"STALLED_MAX_STRIKES": 5,
- "STALLED_IGNORE_PRIVATE": false
+ "STALLED_IGNORE_PRIVATE": false,
+ "STALLED_DELETE_PRIVATE": false
},
"DOWNLOAD_CLIENT": "none",
"qBittorrent": {
diff --git a/code/Infrastructure/Verticals/Arr/ArrClient.cs b/code/Infrastructure/Verticals/Arr/ArrClient.cs
index 797015b3..0713df05 100644
--- a/code/Infrastructure/Verticals/Arr/ArrClient.cs
+++ b/code/Infrastructure/Verticals/Arr/ArrClient.cs
@@ -101,9 +101,9 @@ public abstract class ArrClient
return false;
}
- public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record)
+ public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient)
{
- Uri uri = new(arrInstance.Url, GetQueueDeleteUrlPath(record.Id));
+ Uri uri = new(arrInstance.Url, GetQueueDeleteUrlPath(record.Id, removeFromClient));
using HttpRequestMessage request = new(HttpMethod.Delete, uri);
SetApiKey(request, arrInstance.ApiKey);
@@ -113,8 +113,14 @@ public abstract class ArrClient
try
{
response.EnsureSuccessStatusCode();
-
- _logger.LogInformation("queue item deleted | {url} | {title}", arrInstance.Url, record.Title);
+
+ _logger.LogInformation(
+ removeFromClient
+ ? "queue item deleted | {url} | {title}"
+ : "queue item removed from arr | {url} | {title}",
+ arrInstance.Url,
+ record.Title
+ );
}
catch
{
@@ -144,7 +150,7 @@ public abstract class ArrClient
protected abstract string GetQueueUrlPath(int page);
- protected abstract string GetQueueDeleteUrlPath(long recordId);
+ protected abstract string GetQueueDeleteUrlPath(long recordId, bool removeFromClient);
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
{
diff --git a/code/Infrastructure/Verticals/Arr/LidarrClient.cs b/code/Infrastructure/Verticals/Arr/LidarrClient.cs
index 540d3b8d..ea495183 100644
--- a/code/Infrastructure/Verticals/Arr/LidarrClient.cs
+++ b/code/Infrastructure/Verticals/Arr/LidarrClient.cs
@@ -29,9 +29,13 @@ public sealed class LidarrClient : ArrClient
return $"/api/v1/queue?page={page}&pageSize=200&includeUnknownArtistItems=true&includeArtist=true&includeAlbum=true";
}
- protected override string GetQueueDeleteUrlPath(long recordId)
+ protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
{
- return $"/api/v1/queue/{recordId}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false";
+ string path = $"/api/v1/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
+
+ path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
+
+ return path;
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet? items)
diff --git a/code/Infrastructure/Verticals/Arr/RadarrClient.cs b/code/Infrastructure/Verticals/Arr/RadarrClient.cs
index a223bdcf..488b2b09 100644
--- a/code/Infrastructure/Verticals/Arr/RadarrClient.cs
+++ b/code/Infrastructure/Verticals/Arr/RadarrClient.cs
@@ -29,9 +29,13 @@ public sealed class RadarrClient : ArrClient
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true";
}
- protected override string GetQueueDeleteUrlPath(long recordId)
+ protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
{
- return $"/api/v3/queue/{recordId}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false";
+ string path = $"/api/v3/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
+
+ path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
+
+ return path;
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet? items)
diff --git a/code/Infrastructure/Verticals/Arr/SonarrClient.cs b/code/Infrastructure/Verticals/Arr/SonarrClient.cs
index d78da82a..ddd0295b 100644
--- a/code/Infrastructure/Verticals/Arr/SonarrClient.cs
+++ b/code/Infrastructure/Verticals/Arr/SonarrClient.cs
@@ -29,9 +29,13 @@ public sealed class SonarrClient : ArrClient
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true";
}
- protected override string GetQueueDeleteUrlPath(long recordId)
+ protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
{
- return $"/api/v3/queue/{recordId}?removeFromClient=true&blocklist=true&skipRedownload=true&changeCategory=false";
+ string path = $"/api/v3/queue/{recordId}?blocklist=true&skipRedownload=true&changeCategory=false";
+
+ path += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
+
+ return path;
}
public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet? items)
diff --git a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
index 793e82a1..f4a9f0f0 100644
--- a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
+++ b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
@@ -17,10 +17,12 @@ namespace Infrastructure.Verticals.ContentBlocker;
public sealed class ContentBlocker : GenericHandler
{
+ private readonly ContentBlockerConfig _config;
private readonly BlocklistProvider _blocklistProvider;
public ContentBlocker(
ILogger logger,
+ IOptions config,
IOptions downloadClientConfig,
IOptions sonarrConfig,
IOptions radarrConfig,
@@ -38,6 +40,7 @@ public sealed class ContentBlocker : GenericHandler
arrArrQueueIterator, downloadServiceFactory
)
{
+ _config = config.Value;
_blocklistProvider = blocklistProvider;
}
@@ -96,7 +99,10 @@ public sealed class ContentBlocker : GenericHandler
_logger.LogDebug("searching unwanted files for {title}", record.Title);
- if (!await _downloadService.BlockUnwantedFilesAsync(record.DownloadId, blocklistType, patterns, regexes))
+ BlockFilesResult result = await _downloadService
+ .BlockUnwantedFilesAsync(record.DownloadId, blocklistType, patterns, regexes);
+
+ if (!result.ShouldRemove)
{
continue;
}
@@ -104,7 +110,15 @@ public sealed class ContentBlocker : GenericHandler
_logger.LogDebug("all files are marked as unwanted | {hash}", record.Title);
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
- await arrClient.DeleteQueueItemAsync(instance, record);
+
+ bool removeFromClient = true;
+
+ if (result.IsPrivate && !_config.DeletePrivate)
+ {
+ removeFromClient = false;
+ }
+
+ await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient);
}
});
diff --git a/code/Infrastructure/Verticals/DownloadClient/BlockFilesResult.cs b/code/Infrastructure/Verticals/DownloadClient/BlockFilesResult.cs
new file mode 100644
index 00000000..ed37285b
--- /dev/null
+++ b/code/Infrastructure/Verticals/DownloadClient/BlockFilesResult.cs
@@ -0,0 +1,14 @@
+namespace Infrastructure.Verticals.DownloadClient;
+
+public sealed record BlockFilesResult
+{
+ ///
+ /// True if the download should be removed; otherwise false.
+ ///
+ public bool ShouldRemove { get; set; }
+
+ ///
+ /// True if the download is private; otherwise false.
+ ///
+ public bool IsPrivate { get; set; }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs
index 0bbe1d63..7e22740d 100644
--- a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs
@@ -35,12 +35,12 @@ public sealed class DelugeService : DownloadServiceBase
}
///
- public override async Task ShouldRemoveFromArrQueueAsync(string hash)
+ public override async Task ShouldRemoveFromArrQueueAsync(string hash)
{
hash = hash.ToLowerInvariant();
DelugeContents? contents = null;
- RemoveResult result = new();
+ StalledResult result = new();
TorrentStatus? status = await GetTorrentStatus(hash);
@@ -76,7 +76,7 @@ public sealed class DelugeService : DownloadServiceBase
}
///
- public override async Task BlockUnwantedFilesAsync(
+ public override async Task BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag patterns,
@@ -86,18 +86,21 @@ public sealed class DelugeService : DownloadServiceBase
hash = hash.ToLowerInvariant();
TorrentStatus? status = await GetTorrentStatus(hash);
+ BlockFilesResult result = new();
if (status?.Hash is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
- return false;
+ return result;
}
+
+ result.IsPrivate = status.Private;
if (_contentBlockerConfig.IgnorePrivate && status.Private)
{
// ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", status.Name);
- return false;
+ return result;
}
DelugeContents? contents = null;
@@ -113,7 +116,7 @@ public sealed class DelugeService : DownloadServiceBase
if (contents is null)
{
- return false;
+ return result;
}
Dictionary priorities = [];
@@ -144,7 +147,7 @@ public sealed class DelugeService : DownloadServiceBase
if (!hasPriorityUpdates)
{
- return false;
+ return result;
}
_logger.LogDebug("changing priorities | torrent {hash}", hash);
@@ -157,12 +160,14 @@ public sealed class DelugeService : DownloadServiceBase
if (totalUnwantedFiles == totalFiles)
{
// Skip marking files as unwanted. The download will be removed completely.
- return true;
+ result.ShouldRemove = true;
+
+ return result;
}
await _client.ChangeFilesPriority(hash, sortedPriorities);
- return false;
+ return result;
}
///
diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs
index 0ae5d0ba..87d722db 100644
--- a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs
@@ -37,10 +37,10 @@ public abstract class DownloadServiceBase : IDownloadService
public abstract Task LoginAsync();
- public abstract Task ShouldRemoveFromArrQueueAsync(string hash);
+ public abstract Task ShouldRemoveFromArrQueueAsync(string hash);
///
- public abstract Task BlockUnwantedFilesAsync(
+ public abstract Task BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag patterns,
diff --git a/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs
index 686ad510..19d89b06 100644
--- a/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/DummyDownloadService.cs
@@ -24,12 +24,12 @@ public sealed class DummyDownloadService : DownloadServiceBase
return Task.CompletedTask;
}
- public override Task ShouldRemoveFromArrQueueAsync(string hash)
+ public override Task ShouldRemoveFromArrQueueAsync(string hash)
{
throw new NotImplementedException();
}
- public override Task BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType, ConcurrentBag patterns, ConcurrentBag regexes)
+ public override Task BlockUnwantedFilesAsync(string hash, BlocklistType blocklistType, ConcurrentBag patterns, ConcurrentBag regexes)
{
throw new NotImplementedException();
}
diff --git a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs
index 45218b58..641239fd 100644
--- a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs
@@ -12,7 +12,7 @@ public interface IDownloadService : IDisposable
/// Checks whether the download should be removed from the *arr queue.
///
/// The download hash.
- public Task ShouldRemoveFromArrQueueAsync(string hash);
+ public Task ShouldRemoveFromArrQueueAsync(string hash);
///
/// Blocks unwanted files from being fully downloaded.
@@ -22,7 +22,7 @@ public interface IDownloadService : IDisposable
/// The patterns to test the files against.
/// The regexes to test the files against.
/// True if all files have been blocked; otherwise false.
- public Task BlockUnwantedFilesAsync(
+ public Task BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag patterns,
diff --git a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs
index 706bc5e6..7b2f4236 100644
--- a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs
@@ -43,9 +43,9 @@ public sealed class QBitService : DownloadServiceBase
}
///
- public override async Task ShouldRemoveFromArrQueueAsync(string hash)
+ public override async Task ShouldRemoveFromArrQueueAsync(string hash)
{
- RemoveResult result = new();
+ StalledResult result = new();
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
.FirstOrDefault();
@@ -89,7 +89,7 @@ public sealed class QBitService : DownloadServiceBase
}
///
- public override async Task BlockUnwantedFilesAsync(
+ public override async Task BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag patterns,
@@ -98,11 +98,12 @@ public sealed class QBitService : DownloadServiceBase
{
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
.FirstOrDefault();
+ BlockFilesResult result = new();
if (torrent is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
- return false;
+ return result;
}
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
@@ -110,25 +111,27 @@ public sealed class QBitService : DownloadServiceBase
if (torrentProperties is null)
{
_logger.LogDebug("failed to find torrent properties {hash} in the download client", hash);
- return false;
+ return result;
}
bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
bool.TryParse(dictValue?.ToString(), out bool boolValue)
&& boolValue;
+ result.IsPrivate = isPrivate;
+
if (_contentBlockerConfig.IgnorePrivate && isPrivate)
{
// ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
- return false;
+ return result;
}
IReadOnlyList? files = await _client.GetTorrentContentsAsync(hash);
if (files is null)
{
- return false;
+ return result;
}
List unwantedFiles = [];
@@ -162,13 +165,15 @@ public sealed class QBitService : DownloadServiceBase
if (unwantedFiles.Count is 0)
{
- return false;
+ return result;
}
if (totalUnwantedFiles == totalFiles)
{
// Skip marking files as unwanted. The download will be removed completely.
- return true;
+ result.ShouldRemove = true;
+
+ return result;
}
foreach (int fileIndex in unwantedFiles)
@@ -176,7 +181,7 @@ public sealed class QBitService : DownloadServiceBase
await _client.SetFilePriorityAsync(hash, fileIndex, TorrentContentPriority.Skip);
}
- return false;
+ return result;
}
///
diff --git a/code/Infrastructure/Verticals/DownloadClient/RemoveResult.cs b/code/Infrastructure/Verticals/DownloadClient/StalledResult.cs
similarity index 90%
rename from code/Infrastructure/Verticals/DownloadClient/RemoveResult.cs
rename to code/Infrastructure/Verticals/DownloadClient/StalledResult.cs
index 75efa446..ead7572b 100644
--- a/code/Infrastructure/Verticals/DownloadClient/RemoveResult.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/StalledResult.cs
@@ -1,6 +1,6 @@
namespace Infrastructure.Verticals.DownloadClient;
-public sealed record RemoveResult
+public sealed record StalledResult
{
///
/// True if the download should be removed; otherwise false.
diff --git a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs
index 04c6f673..5999a643 100644
--- a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs
@@ -46,9 +46,9 @@ public sealed class TransmissionService : DownloadServiceBase
}
///
- public override async Task ShouldRemoveFromArrQueueAsync(string hash)
+ public override async Task ShouldRemoveFromArrQueueAsync(string hash)
{
- RemoveResult result = new();
+ StalledResult result = new();
TorrentInfo? torrent = await GetTorrentAsync(hash);
if (torrent is null)
@@ -82,7 +82,7 @@ public sealed class TransmissionService : DownloadServiceBase
}
///
- public override async Task BlockUnwantedFilesAsync(
+ public override async Task BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag patterns,
@@ -90,17 +90,21 @@ public sealed class TransmissionService : DownloadServiceBase
)
{
TorrentInfo? torrent = await GetTorrentAsync(hash);
+ BlockFilesResult result = new();
if (torrent?.FileStats is null || torrent.Files is null)
{
- return false;
+ return result;
}
+
+ bool isPrivate = torrent.IsPrivate ?? false;
+ result.IsPrivate = isPrivate;
- if (_contentBlockerConfig.IgnorePrivate && (torrent.IsPrivate ?? false))
+ if (_contentBlockerConfig.IgnorePrivate && isPrivate)
{
// ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
- return false;
+ return result;
}
List unwantedFiles = [];
@@ -134,13 +138,15 @@ public sealed class TransmissionService : DownloadServiceBase
if (unwantedFiles.Count is 0)
{
- return false;
+ return result;
}
if (totalUnwantedFiles == totalFiles)
{
// Skip marking files as unwanted. The download will be removed completely.
- return true;
+ result.ShouldRemove = true;
+
+ return result;
}
_logger.LogDebug("changing priorities | torrent {hash}", hash);
@@ -151,7 +157,7 @@ public sealed class TransmissionService : DownloadServiceBase
FilesUnwanted = unwantedFiles.ToArray(),
});
- return false;
+ return result;
}
public override async Task Delete(string hash)
diff --git a/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs b/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs
index 77fc347b..76155615 100644
--- a/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs
+++ b/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs
@@ -1,5 +1,6 @@
using Common.Configuration.Arr;
using Common.Configuration.DownloadClient;
+using Common.Configuration.QueueCleaner;
using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
@@ -14,8 +15,11 @@ namespace Infrastructure.Verticals.QueueCleaner;
public sealed class QueueCleaner : GenericHandler
{
+ private readonly QueueCleanerConfig _config;
+
public QueueCleaner(
ILogger logger,
+ IOptions config,
IOptions downloadClientConfig,
IOptions sonarrConfig,
IOptions radarrConfig,
@@ -32,6 +36,7 @@ public sealed class QueueCleaner : GenericHandler
arrArrQueueIterator, downloadServiceFactory
)
{
+ _config = config.Value;
}
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
@@ -66,23 +71,41 @@ public sealed class QueueCleaner : GenericHandler
continue;
}
- RemoveResult removeResult = new();
+ StalledResult stalledCheckResult = new();
if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None)
{
- removeResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
+ // stalled download check
+ stalledCheckResult = await _downloadService.ShouldRemoveFromArrQueueAsync(record.DownloadId);
}
- bool shouldRemoveFromArr = arrClient.ShouldRemoveFromQueue(record, removeResult.IsPrivate);
+ // failed import check
+ bool shouldRemoveFromArr = arrClient.ShouldRemoveFromQueue(record, stalledCheckResult.IsPrivate);
- if (!shouldRemoveFromArr && !removeResult.ShouldRemove)
+ if (!shouldRemoveFromArr && !stalledCheckResult.ShouldRemove)
{
_logger.LogInformation("skip | {title}", record.Title);
continue;
}
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1));
- await arrClient.DeleteQueueItemAsync(instance, record);
+
+ bool removeFromClient = true;
+
+ if (stalledCheckResult.IsPrivate)
+ {
+ if (stalledCheckResult.ShouldRemove && !_config.StalledDeletePrivate)
+ {
+ removeFromClient = false;
+ }
+
+ if (shouldRemoveFromArr && !_config.ImportFailedDeletePrivate)
+ {
+ removeFromClient = false;
+ }
+ }
+
+ await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient);
}
});
diff --git a/code/test/docker-compose.yml b/code/test/docker-compose.yml
index 17c7861e..d0ac73ae 100644
--- a/code/test/docker-compose.yml
+++ b/code/test/docker-compose.yml
@@ -186,12 +186,15 @@ services:
- QUEUECLEANER__RUNSEQUENTIALLY=true
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true
+ - QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample
- QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=true
+ - QUEUECLEANER__STALLED_DELETE_PRIVATE=false
- CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__IGNORE_PRIVATE=true
+ - CONTENTBLOCKER__DELETE_PRIVATE=false
- DOWNLOAD_CLIENT=qbittorrent
- QBITTORRENT__URL=http://qbittorrent:8080