Add option to not remove private downloads from the download client (#45)

This commit is contained in:
Marius Nechifor
2025-01-18 17:20:23 +02:00
committed by GitHub
parent b88ddde417
commit 8a8b906b6f
20 changed files with 181 additions and 73 deletions
+11 -5
View File
@@ -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)
{
@@ -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<SearchItem>? items)
@@ -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<SearchItem>? items)
@@ -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<SearchItem>? items)
@@ -17,10 +17,12 @@ namespace Infrastructure.Verticals.ContentBlocker;
public sealed class ContentBlocker : GenericHandler
{
private readonly ContentBlockerConfig _config;
private readonly BlocklistProvider _blocklistProvider;
public ContentBlocker(
ILogger<ContentBlocker> logger,
IOptions<ContentBlockerConfig> config,
IOptions<DownloadClientConfig> downloadClientConfig,
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> 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);
}
});
@@ -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/>
public override async Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
public override async Task<StalledResult> 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
}
/// <inheritdoc/>
public override async Task<bool> BlockUnwantedFilesAsync(
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag<string> 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<int, int> 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;
}
/// <inheritdoc/>
@@ -37,10 +37,10 @@ public abstract class DownloadServiceBase : IDownloadService
public abstract Task LoginAsync();
public abstract Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);
public abstract Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
/// <inheritdoc/>
public abstract Task<bool> BlockUnwantedFilesAsync(
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag<string> patterns,
@@ -24,12 +24,12 @@ public sealed class DummyDownloadService : DownloadServiceBase
return Task.CompletedTask;
}
public override Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash)
public override Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash)
{
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();
}
@@ -12,7 +12,7 @@ public interface IDownloadService : IDisposable
/// Checks whether the download should be removed from the *arr queue.
/// </summary>
/// <param name="hash">The download hash.</param>
public Task<RemoveResult> ShouldRemoveFromArrQueueAsync(string hash);
public Task<StalledResult> ShouldRemoveFromArrQueueAsync(string hash);
/// <summary>
/// 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="regexes">The regexes to test the files against.</param>
/// <returns>True if all files have been blocked; otherwise false.</returns>
public Task<bool> BlockUnwantedFilesAsync(
public Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag<string> patterns,
@@ -43,9 +43,9 @@ public sealed class QBitService : DownloadServiceBase
}
/// <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] }))
.FirstOrDefault();
@@ -89,7 +89,7 @@ public sealed class QBitService : DownloadServiceBase
}
/// <inheritdoc/>
public override async Task<bool> BlockUnwantedFilesAsync(
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag<string> 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<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
if (files is null)
{
return false;
return result;
}
List<int> 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;
}
/// <inheritdoc/>
@@ -1,6 +1,6 @@
namespace Infrastructure.Verticals.DownloadClient;
public sealed record RemoveResult
public sealed record StalledResult
{
/// <summary>
/// True if the download should be removed; otherwise false.
@@ -46,9 +46,9 @@ public sealed class TransmissionService : DownloadServiceBase
}
/// <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);
if (torrent is null)
@@ -82,7 +82,7 @@ public sealed class TransmissionService : DownloadServiceBase
}
/// <inheritdoc/>
public override async Task<bool> BlockUnwantedFilesAsync(
public override async Task<BlockFilesResult> BlockUnwantedFilesAsync(
string hash,
BlocklistType blocklistType,
ConcurrentBag<string> 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<long> 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)
@@ -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<QueueCleaner> logger,
IOptions<QueueCleanerConfig> config,
IOptions<DownloadClientConfig> downloadClientConfig,
IOptions<SonarrConfig> sonarrConfig,
IOptions<RadarrConfig> 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);
}
});