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