Compare commits

...

3 Commits

Author SHA1 Message Date
Marius Nechifor 209f78717f Fix usenet usage (#46) 2025-01-18 19:12:28 +02:00
Flaminel a02be80ac1 updated README 2025-01-18 17:25:15 +02:00
Marius Nechifor 8a8b906b6f Add option to not remove private downloads from the download client (#45) 2025-01-18 17:20:23 +02:00
20 changed files with 182 additions and 79 deletions
+22 -16
View File
@@ -106,13 +106,16 @@ services:
- QUEUECLEANER__RUNSEQUENTIALLY=true - QUEUECLEANER__RUNSEQUENTIALLY=true
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5 - QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=false - 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__0=title mismatch
# - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required # - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__1=manual import required
- QUEUECLEANER__STALLED_MAX_STRIKES=5 - QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=false - QUEUECLEANER__STALLED_IGNORE_PRIVATE=false
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
- CONTENTBLOCKER__ENABLED=true - CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__IGNORE_PRIVATE=true - CONTENTBLOCKER__IGNORE_PRIVATE=false
- CONTENTBLOCKER__DELETE_PRIVATE=false
- DOWNLOAD_CLIENT=none - DOWNLOAD_CLIENT=none
# OR # OR
@@ -166,24 +169,27 @@ services:
| Variable | Required | Description | Default value | | Variable | Required | Description | Default value |
|---|---|---|---| |---|---|---|---|
| LOGGING__LOGLEVEL | No | Can be `Verbose`, `Debug`, `Information`, `Warning`, `Error` or `Fatal` | `Information` | | 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__ENABLED | No | Enable or disable logging to file. | false |
| LOGGING__FILE__PATH | No | Directory where to save the log files | empty | | LOGGING__FILE__PATH | No | Directory where to save the log files. | empty |
| LOGGING__ENHANCED | No | Enhance logs whenever possible<br>A more detailed description is provided [here](variables.md#LOGGING__ENHANCED) | true | | LOGGING__ENHANCED | No | Enhance logs whenever possible.<br>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)<br>Can be a max of 6h interval<br>**Is ignored if `QUEUECLEANER__RUNSEQUENTIALLY=true` and `CONTENTBLOCKER__ENABLED=true`** | 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).<br>- Can be a max of 6h interval.<br>- **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)<br>Can be a max of 6h interval | 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).<br>- Can be a max of 6h interval. | 0 0/5 * * * ? |
||||| |||||
| QUEUECLEANER__ENABLED | No | Enable or disable the queue cleaner | true | | 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__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<br>0 means never | 0 | | QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES | No | - After how many strikes should a failed import be removed.<br>- 0 means never. | 0 |
| QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE | No | Whether to ignore failed imports from private trackers | false | | 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<br>If the specified message pattern is found, the item is skipped | empty | | QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE | No | - Whether to delete failed imports of private downloads from the download client.<br>- Does not have any effect if `QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE` is `true`.<br>- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
| QUEUECLEANER__STALLED_MAX_STRIKES | No | After how many strikes should a stalled download be removed<br>0 means never | 0 | | QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0 | No | - First pattern to look for when an import is failed.<br>- If the specified message pattern is found, the item is skipped. | empty |
| QUEUECLEANER__STALLED_IGNORE_PRIVATE | No | Whether to ignore stalled downloads from private trackers | false | | QUEUECLEANER__STALLED_MAX_STRIKES | No | - After how many strikes should a stalled download be removed.<br>- 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 private downloads from the download client.<br>- Does not have any effect if `QUEUECLEANER__STALLED_IGNORE_PRIVATE` is `true`.<br>- **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__ENABLED | No | Enable or disable the content blocker. | false |
| CONTENTBLOCKER__IGNORE_PRIVATE | No | Whether to ignore downloads from private trackers | false | | CONTENTBLOCKER__IGNORE_PRIVATE | No | Whether to ignore downloads from private trackers. | false |
| CONTENTBLOCKER__DELETE_PRIVATE | No | - Whether to delete private downloads that have all files blocked from the download client.<br>- Does not have any effect if `CONTENTBLOCKER__IGNORE_PRIVATE` is `true`.<br>- **Set this to `true` if you don't care about seeding, ratio, H&R and potentially losing your tracker account.** | false |
</details> </details>
### Download client variables ### Download client variables
@@ -1,4 +1,4 @@
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
namespace Common.Configuration.ContentBlocker; namespace Common.Configuration.ContentBlocker;
@@ -11,6 +11,9 @@ public sealed record ContentBlockerConfig : IJobConfig
[ConfigurationKeyName("IGNORE_PRIVATE")] [ConfigurationKeyName("IGNORE_PRIVATE")]
public bool IgnorePrivate { get; init; } public bool IgnorePrivate { get; init; }
[ConfigurationKeyName("DELETE_PRIVATE")]
public bool DeletePrivate { get; init; }
public void Validate() public void Validate()
{ {
} }
@@ -16,6 +16,9 @@ public sealed record QueueCleanerConfig : IJobConfig
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PRIVATE")] [ConfigurationKeyName("IMPORT_FAILED_IGNORE_PRIVATE")]
public bool ImportFailedIgnorePrivate { get; init; } public bool ImportFailedIgnorePrivate { get; init; }
[ConfigurationKeyName("IMPORT_FAILED_DELETE_PRIVATE")]
public bool ImportFailedDeletePrivate { get; init; }
[ConfigurationKeyName("IMPORT_FAILED_IGNORE_PATTERNS")] [ConfigurationKeyName("IMPORT_FAILED_IGNORE_PATTERNS")]
public List<string>? ImportFailedIgnorePatterns { get; init; } public List<string>? ImportFailedIgnorePatterns { get; init; }
@@ -25,6 +28,9 @@ public sealed record QueueCleanerConfig : IJobConfig
[ConfigurationKeyName("STALLED_IGNORE_PRIVATE")] [ConfigurationKeyName("STALLED_IGNORE_PRIVATE")]
public bool StalledIgnorePrivate { get; init; } public bool StalledIgnorePrivate { get; init; }
[ConfigurationKeyName("STALLED_DELETE_PRIVATE")]
public bool StalledDeletePrivate { get; init; }
public void Validate() public void Validate()
{ {
} }
+5 -2
View File
@@ -15,18 +15,21 @@
}, },
"ContentBlocker": { "ContentBlocker": {
"Enabled": true, "Enabled": true,
"IGNORE_PRIVATE": true "IGNORE_PRIVATE": true,
"DELETE_PRIVATE": false
}, },
"QueueCleaner": { "QueueCleaner": {
"Enabled": true, "Enabled": true,
"RunSequentially": true, "RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5, "IMPORT_FAILED_MAX_STRIKES": 5,
"IMPORT_FAILED_IGNORE_PRIVATE": true, "IMPORT_FAILED_IGNORE_PRIVATE": true,
"IMPORT_FAILED_DELETE_PRIVATE": false,
"IMPORT_FAILED_IGNORE_PATTERNS": [ "IMPORT_FAILED_IGNORE_PATTERNS": [
"file is a sample" "file is a sample"
], ],
"STALLED_MAX_STRIKES": 5, "STALLED_MAX_STRIKES": 5,
"STALLED_IGNORE_PRIVATE": true "STALLED_IGNORE_PRIVATE": true,
"STALLED_DELETE_PRIVATE": false
}, },
"DOWNLOAD_CLIENT": "qbittorrent", "DOWNLOAD_CLIENT": "qbittorrent",
"qBittorrent": { "qBittorrent": {
+3 -1
View File
@@ -22,9 +22,11 @@
"RunSequentially": true, "RunSequentially": true,
"IMPORT_FAILED_MAX_STRIKES": 5, "IMPORT_FAILED_MAX_STRIKES": 5,
"IMPORT_FAILED_IGNORE_PRIVATE": false, "IMPORT_FAILED_IGNORE_PRIVATE": false,
"IMPORT_FAILED_DELETE_PRIVATE": false,
"IMPORT_FAILED_IGNORE_PATTERNS": [], "IMPORT_FAILED_IGNORE_PATTERNS": [],
"STALLED_MAX_STRIKES": 5, "STALLED_MAX_STRIKES": 5,
"STALLED_IGNORE_PRIVATE": false "STALLED_IGNORE_PRIVATE": false,
"STALLED_DELETE_PRIVATE": false
}, },
"DOWNLOAD_CLIENT": "none", "DOWNLOAD_CLIENT": "none",
"qBittorrent": { "qBittorrent": {
+10 -4
View File
@@ -101,9 +101,9 @@ public abstract class ArrClient
return false; 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); using HttpRequestMessage request = new(HttpMethod.Delete, uri);
SetApiKey(request, arrInstance.ApiKey); SetApiKey(request, arrInstance.ApiKey);
@@ -114,7 +114,13 @@ public abstract class ArrClient
{ {
response.EnsureSuccessStatusCode(); 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 catch
{ {
@@ -144,7 +150,7 @@ public abstract class ArrClient
protected abstract string GetQueueUrlPath(int page); 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) protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
{ {
@@ -29,9 +29,13 @@ public sealed class LidarrClient : ArrClient
return $"/api/v1/queue?page={page}&pageSize=200&includeUnknownArtistItems=true&includeArtist=true&includeAlbum=true"; 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<SearchItem>? items) public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
@@ -29,9 +29,13 @@ public sealed class RadarrClient : ArrClient
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownMovieItems=true&includeMovie=true"; 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<SearchItem>? items) public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
@@ -29,9 +29,13 @@ public sealed class SonarrClient : ArrClient
return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true"; 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<SearchItem>? items) public override async Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
@@ -17,10 +17,12 @@ namespace Infrastructure.Verticals.ContentBlocker;
public sealed class ContentBlocker : GenericHandler public sealed class ContentBlocker : GenericHandler
{ {
private readonly ContentBlockerConfig _config;
private readonly BlocklistProvider _blocklistProvider; private readonly BlocklistProvider _blocklistProvider;
public ContentBlocker( public ContentBlocker(
ILogger<ContentBlocker> logger, ILogger<ContentBlocker> logger,
IOptions<ContentBlockerConfig> config,
IOptions<DownloadClientConfig> downloadClientConfig, IOptions<DownloadClientConfig> downloadClientConfig,
IOptions<SonarrConfig> sonarrConfig, IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig, IOptions<RadarrConfig> radarrConfig,
@@ -38,6 +40,7 @@ public sealed class ContentBlocker : GenericHandler
arrArrQueueIterator, downloadServiceFactory arrArrQueueIterator, downloadServiceFactory
) )
{ {
_config = config.Value;
_blocklistProvider = blocklistProvider; _blocklistProvider = blocklistProvider;
} }
@@ -96,7 +99,10 @@ public sealed class ContentBlocker : GenericHandler
_logger.LogDebug("searching unwanted files for {title}", record.Title); _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; continue;
} }
@@ -104,7 +110,15 @@ public sealed class ContentBlocker : GenericHandler
_logger.LogDebug("all files are marked as unwanted | {hash}", record.Title); _logger.LogDebug("all files are marked as unwanted | {hash}", record.Title);
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1)); 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);
} }
}); });
@@ -0,0 +1,14 @@
namespace Infrastructure.Verticals.DownloadClient;
public sealed record BlockFilesResult
{
/// <summary>
/// True if the download should be removed; otherwise false.
/// </summary>
public bool ShouldRemove { get; set; }
/// <summary>
/// True if the download is private; otherwise false.
/// </summary>
public bool IsPrivate { get; set; }
}
@@ -35,12 +35,12 @@ public sealed class DelugeService : DownloadServiceBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash) public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
{ {
hash = hash.ToLowerInvariant(); hash = hash.ToLowerInvariant();
DelugeContents? contents = null; DelugeContents? contents = null;
RemoveResult result = new(); StalledResult result = new();
TorrentStatus? status = await GetTorrentStatus(hash); TorrentStatus? status = await GetTorrentStatus(hash);
@@ -76,7 +76,7 @@ public sealed class DelugeService : DownloadServiceBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<bool> BlockUnwantedFilesAsync( public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash, string hash,
BlocklistType blocklistType, BlocklistType blocklistType,
ConcurrentBag<string> patterns, ConcurrentBag<string> patterns,
@@ -86,18 +86,21 @@ public sealed class DelugeService : DownloadServiceBase
hash = hash.ToLowerInvariant(); hash = hash.ToLowerInvariant();
TorrentStatus? status = await GetTorrentStatus(hash); TorrentStatus? status = await GetTorrentStatus(hash);
BlockFilesResult result = new();
if (status?.Hash is null) if (status?.Hash is null)
{ {
_logger.LogDebug("failed to find torrent {hash} in the download client", hash); _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) if (_contentBlockerConfig.IgnorePrivate && status.Private)
{ {
// ignore private trackers // ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", status.Name); _logger.LogDebug("skip files check | download is private | {name}", status.Name);
return false; return result;
} }
DelugeContents? contents = null; DelugeContents? contents = null;
@@ -113,7 +116,7 @@ public sealed class DelugeService : DownloadServiceBase
if (contents is null) if (contents is null)
{ {
return false; return result;
} }
Dictionary<int, int> priorities = []; Dictionary<int, int> priorities = [];
@@ -144,7 +147,7 @@ public sealed class DelugeService : DownloadServiceBase
if (!hasPriorityUpdates) if (!hasPriorityUpdates)
{ {
return false; return result;
} }
_logger.LogDebug("changing priorities | torrent {hash}", hash); _logger.LogDebug("changing priorities | torrent {hash}", hash);
@@ -157,12 +160,14 @@ public sealed class DelugeService : DownloadServiceBase
if (totalUnwantedFiles == totalFiles) if (totalUnwantedFiles == totalFiles)
{ {
// Skip marking files as unwanted. The download will be removed completely. // Skip marking files as unwanted. The download will be removed completely.
return true; result.ShouldRemove = true;
return result;
} }
await _client.ChangeFilesPriority(hash, sortedPriorities); await _client.ChangeFilesPriority(hash, sortedPriorities);
return false; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -37,10 +37,10 @@ public abstract class DownloadServiceBase : IDownloadService
public abstract Task LoginAsync(); public abstract Task LoginAsync();
public abstract Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash); public abstract Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
/// <inheritdoc/> /// <inheritdoc/>
public abstract Task<bool> BlockUnwantedFilesAsync( public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash, string hash,
BlocklistType blocklistType, BlocklistType blocklistType,
ConcurrentBag<string> patterns, ConcurrentBag<string> patterns,
@@ -24,12 +24,12 @@ public sealed class DummyDownloadService : DownloadServiceBase
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash) public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task<bool> 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)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@@ -12,7 +12,7 @@ public interface IDownloadService : IDisposable
/// Checks whether the download should be removed from the *arr queue. /// Checks whether the download should be removed from the *arr queue.
/// </summary> /// </summary>
/// <param name="hash">The download hash.</param> /// <param name="hash">The download hash.</param>
public Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash); public Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
/// <summary> /// <summary>
/// Blocks unwanted files from being fully downloaded. /// Blocks unwanted files from being fully downloaded.
@@ -22,7 +22,7 @@ public interface IDownloadService : IDisposable
/// <param name="patterns">The patterns to test the files against.</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="regexes">The regexes to test the files against.</param>
/// <returns>True if all files have been blocked; otherwise false.</returns> /// <returns>True if all files have been blocked; otherwise false.</returns>
public Task<bool> BlockUnwantedFilesAsync( public Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash, string hash,
BlocklistType blocklistType, BlocklistType blocklistType,
ConcurrentBag<string> patterns, ConcurrentBag<string> patterns,
@@ -43,9 +43,9 @@ public sealed class QBitService : DownloadServiceBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash) public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
{ {
RemoveResult result = new(); StalledResult result = new();
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] })) TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
.FirstOrDefault(); .FirstOrDefault();
@@ -89,7 +89,7 @@ public sealed class QBitService : DownloadServiceBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<bool> BlockUnwantedFilesAsync( public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash, string hash,
BlocklistType blocklistType, BlocklistType blocklistType,
ConcurrentBag<string> patterns, ConcurrentBag<string> patterns,
@@ -98,11 +98,12 @@ public sealed class QBitService : DownloadServiceBase
{ {
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] })) TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
.FirstOrDefault(); .FirstOrDefault();
BlockFilesResult result = new();
if (torrent is null) if (torrent is null)
{ {
_logger.LogDebug("failed to find torrent {hash} in the download client", hash); _logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return false; return result;
} }
TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash); TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash);
@@ -110,25 +111,27 @@ public sealed class QBitService : DownloadServiceBase
if (torrentProperties is null) if (torrentProperties is null)
{ {
_logger.LogDebug("failed to find torrent properties {hash} in the download client", hash); _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 isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) &&
bool.TryParse(dictValue?.ToString(), out bool boolValue) bool.TryParse(dictValue?.ToString(), out bool boolValue)
&& boolValue; && boolValue;
result.IsPrivate = isPrivate;
if (_contentBlockerConfig.IgnorePrivate && isPrivate) if (_contentBlockerConfig.IgnorePrivate && isPrivate)
{ {
// ignore private trackers // ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name); _logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
return false; return result;
} }
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash); IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
if (files is null) if (files is null)
{ {
return false; return result;
} }
List<int> unwantedFiles = []; List<int> unwantedFiles = [];
@@ -162,13 +165,15 @@ public sealed class QBitService : DownloadServiceBase
if (unwantedFiles.Count is 0) if (unwantedFiles.Count is 0)
{ {
return false; return result;
} }
if (totalUnwantedFiles == totalFiles) if (totalUnwantedFiles == totalFiles)
{ {
// Skip marking files as unwanted. The download will be removed completely. // Skip marking files as unwanted. The download will be removed completely.
return true; result.ShouldRemove = true;
return result;
} }
foreach (int fileIndex in unwantedFiles) foreach (int fileIndex in unwantedFiles)
@@ -176,7 +181,7 @@ public sealed class QBitService : DownloadServiceBase
await _client.SetFilePriorityAsync(hash, fileIndex, TorrentContentPriority.Skip); await _client.SetFilePriorityAsync(hash, fileIndex, TorrentContentPriority.Skip);
} }
return false; return result;
} }
/// <inheritdoc/> /// <inheritdoc/>
@@ -1,6 +1,6 @@
namespace Infrastructure.Verticals.DownloadClient; namespace Infrastructure.Verticals.DownloadClient;
public sealed record RemoveResult public sealed record StalledResult
{ {
/// <summary> /// <summary>
/// True if the download should be removed; otherwise false. /// True if the download should be removed; otherwise false.
@@ -46,9 +46,9 @@ public sealed class TransmissionService : DownloadServiceBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash) public override async Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
{ {
RemoveResult result = new(); StalledResult result = new();
TorrentInfo? torrent = await GetTorrentAsync(hash); TorrentInfo? torrent = await GetTorrentAsync(hash);
if (torrent is null) if (torrent is null)
@@ -82,7 +82,7 @@ public sealed class TransmissionService : DownloadServiceBase
} }
/// <inheritdoc/> /// <inheritdoc/>
public override async Task<bool> BlockUnwantedFilesAsync( public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash, string hash,
BlocklistType blocklistType, BlocklistType blocklistType,
ConcurrentBag<string> patterns, ConcurrentBag<string> patterns,
@@ -90,17 +90,21 @@ public sealed class TransmissionService : DownloadServiceBase
) )
{ {
TorrentInfo? torrent = await GetTorrentAsync(hash); TorrentInfo? torrent = await GetTorrentAsync(hash);
BlockFilesResult result = new();
if (torrent?.FileStats is null || torrent.Files is null) if (torrent?.FileStats is null || torrent.Files is null)
{ {
return false; return result;
} }
if (_contentBlockerConfig.IgnorePrivate && (torrent.IsPrivate ?? false)) bool isPrivate = torrent.IsPrivate ?? false;
result.IsPrivate = isPrivate;
if (_contentBlockerConfig.IgnorePrivate && isPrivate)
{ {
// ignore private trackers // ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", torrent.Name); _logger.LogDebug("skip files check | download is private | {name}", torrent.Name);
return false; return result;
} }
List<long> unwantedFiles = []; List<long> unwantedFiles = [];
@@ -134,13 +138,15 @@ public sealed class TransmissionService : DownloadServiceBase
if (unwantedFiles.Count is 0) if (unwantedFiles.Count is 0)
{ {
return false; return result;
} }
if (totalUnwantedFiles == totalFiles) if (totalUnwantedFiles == totalFiles)
{ {
// Skip marking files as unwanted. The download will be removed completely. // 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); _logger.LogDebug("changing priorities | torrent {hash}", hash);
@@ -151,7 +157,7 @@ public sealed class TransmissionService : DownloadServiceBase
FilesUnwanted = unwantedFiles.ToArray(), FilesUnwanted = unwantedFiles.ToArray(),
}); });
return false; return result;
} }
public override async Task Delete(string hash) public override async Task Delete(string hash)
@@ -1,5 +1,6 @@
using Common.Configuration.Arr; using Common.Configuration.Arr;
using Common.Configuration.DownloadClient; using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner;
using Domain.Enums; using Domain.Enums;
using Domain.Models.Arr; using Domain.Models.Arr;
using Domain.Models.Arr.Queue; using Domain.Models.Arr.Queue;
@@ -14,8 +15,11 @@ namespace Infrastructure.Verticals.QueueCleaner;
public sealed class QueueCleaner : GenericHandler public sealed class QueueCleaner : GenericHandler
{ {
private readonly QueueCleanerConfig _config;
public QueueCleaner( public QueueCleaner(
ILogger<QueueCleaner> logger, ILogger<QueueCleaner> logger,
IOptions<QueueCleanerConfig> config,
IOptions<DownloadClientConfig> downloadClientConfig, IOptions<DownloadClientConfig> downloadClientConfig,
IOptions<SonarrConfig> sonarrConfig, IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> radarrConfig, IOptions<RadarrConfig> radarrConfig,
@@ -32,6 +36,7 @@ public sealed class QueueCleaner : GenericHandler
arrArrQueueIterator, downloadServiceFactory arrArrQueueIterator, downloadServiceFactory
) )
{ {
_config = config.Value;
} }
protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType) protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
@@ -56,33 +61,46 @@ public sealed class QueueCleaner : GenericHandler
QueueRecord record = group.First(); QueueRecord record = group.First();
if (record.Protocol is not "torrent")
{
continue;
}
if (!arrClient.IsRecordValid(record)) if (!arrClient.IsRecordValid(record))
{ {
continue; continue;
} }
RemoveResult removeResult = new(); StalledResult stalledCheckResult = new();
if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None) if (_downloadClientConfig.DownloadClient is not Common.Enums.DownloadClient.None && record.Protocol is "torrent")
{ {
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); _logger.LogInformation("skip | {title}", record.Title);
continue; continue;
} }
itemsToBeRefreshed.Add(GetRecordSearchItem(instanceType, record, group.Count() > 1)); 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);
} }
}); });
+3
View File
@@ -186,12 +186,15 @@ services:
- QUEUECLEANER__RUNSEQUENTIALLY=true - QUEUECLEANER__RUNSEQUENTIALLY=true
- QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5 - QUEUECLEANER__IMPORT_FAILED_MAX_STRIKES=5
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true - QUEUECLEANER__IMPORT_FAILED_IGNORE_PRIVATE=true
- QUEUECLEANER__IMPORT_FAILED_DELETE_PRIVATE=false
- QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample - QUEUECLEANER__IMPORT_FAILED_IGNORE_PATTERNS__0=file is a sample
- QUEUECLEANER__STALLED_MAX_STRIKES=5 - QUEUECLEANER__STALLED_MAX_STRIKES=5
- QUEUECLEANER__STALLED_IGNORE_PRIVATE=true - QUEUECLEANER__STALLED_IGNORE_PRIVATE=true
- QUEUECLEANER__STALLED_DELETE_PRIVATE=false
- CONTENTBLOCKER__ENABLED=true - CONTENTBLOCKER__ENABLED=true
- CONTENTBLOCKER__IGNORE_PRIVATE=true - CONTENTBLOCKER__IGNORE_PRIVATE=true
- CONTENTBLOCKER__DELETE_PRIVATE=false
- DOWNLOAD_CLIENT=qbittorrent - DOWNLOAD_CLIENT=qbittorrent
- QBITTORRENT__URL=http://qbittorrent:8080 - QBITTORRENT__URL=http://qbittorrent:8080