Remove stalled downloads (#21)
This commit is contained in:
@@ -1,44 +1,46 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Domain.Models.Deluge.Response;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.ItemStriker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Deluge;
|
||||
|
||||
public sealed class DelugeService : IDownloadService
|
||||
public sealed class DelugeService : DownloadServiceBase
|
||||
{
|
||||
private readonly ILogger<DelugeService> _logger;
|
||||
private readonly DelugeClient _client;
|
||||
private readonly FilenameEvaluator _filenameEvaluator;
|
||||
|
||||
public DelugeService(
|
||||
ILogger<DelugeService> logger,
|
||||
IOptions<DelugeConfig> config,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
FilenameEvaluator filenameEvaluator
|
||||
)
|
||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||
FilenameEvaluator filenameEvaluator,
|
||||
Striker striker
|
||||
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
||||
{
|
||||
_logger = logger;
|
||||
config.Value.Validate();
|
||||
_client = new (config, httpClientFactory);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
public override async Task LoginAsync()
|
||||
{
|
||||
await _client.LoginAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
DelugeContents? contents = null;
|
||||
|
||||
TorrentStatus? status = await GetTorrentStatus(hash);
|
||||
|
||||
if (!await HasMinimalStatus(hash))
|
||||
if (status?.Hash is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -51,13 +53,7 @@ public sealed class DelugeService : IDownloadService
|
||||
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
|
||||
}
|
||||
|
||||
// if no files found, torrent might be stuck in Downloading metadata
|
||||
if (contents?.Contents?.Count is null or 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool shouldRemove = true;
|
||||
bool shouldRemove = contents?.Contents?.Count > 0;
|
||||
|
||||
ProcessFiles(contents.Contents, (_, file) =>
|
||||
{
|
||||
@@ -67,15 +63,18 @@ public sealed class DelugeService : IDownloadService
|
||||
}
|
||||
});
|
||||
|
||||
return shouldRemove;
|
||||
return shouldRemove || IsItemStuckAndShouldRemove(status);
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
hash = hash.ToLowerInvariant();
|
||||
|
||||
if (!await HasMinimalStatus(hash))
|
||||
TorrentStatus? status = await GetTorrentStatus(hash);
|
||||
|
||||
if (status?.Hash is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,22 +125,29 @@ public sealed class DelugeService : IDownloadService
|
||||
|
||||
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
||||
}
|
||||
|
||||
private async Task<bool> HasMinimalStatus(string hash)
|
||||
|
||||
private bool IsItemStuckAndShouldRemove(TorrentStatus status)
|
||||
{
|
||||
DelugeMinimalStatus? status = await _client.SendRequest<DelugeMinimalStatus?>(
|
||||
"web.get_torrent_status",
|
||||
hash,
|
||||
new[] { "hash" }
|
||||
);
|
||||
|
||||
if (status?.Hash is null)
|
||||
if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (status.Eta > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return StrikeAndCheckLimit(status.Hash!, status.Name!);
|
||||
}
|
||||
|
||||
private async Task<TorrentStatus?> GetTorrentStatus(string hash)
|
||||
{
|
||||
return await _client.SendRequest<TorrentStatus?>(
|
||||
"web.get_torrent_status",
|
||||
hash,
|
||||
new[] { "hash", "state", "name", "eta" }
|
||||
);
|
||||
}
|
||||
|
||||
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory> contents, Action<string, DelugeFileOrDirectory> processFile)
|
||||
@@ -161,7 +167,7 @@ public sealed class DelugeService : IDownloadService
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public override void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.ItemStriker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient;
|
||||
|
||||
public abstract class DownloadServiceBase : IDownloadService
|
||||
{
|
||||
protected readonly ILogger<DownloadServiceBase> _logger;
|
||||
protected readonly QueueCleanerConfig _queueCleanerConfig;
|
||||
protected readonly FilenameEvaluator _filenameEvaluator;
|
||||
protected readonly Striker _striker;
|
||||
|
||||
protected DownloadServiceBase(
|
||||
ILogger<DownloadServiceBase> logger,
|
||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||
FilenameEvaluator filenameEvaluator,
|
||||
Striker striker
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_queueCleanerConfig = queueCleanerConfig.Value;
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
_striker = striker;
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract Task LoginAsync();
|
||||
|
||||
public abstract Task<bool> ShouldRemoveFromArrQueueAsync(string hash);
|
||||
|
||||
public abstract Task BlockUnwantedFilesAsync(string hash);
|
||||
|
||||
protected bool StrikeAndCheckLimit(string hash, string itemName)
|
||||
{
|
||||
return _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.ItemStriker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using QBittorrent.Client;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.QBittorrent;
|
||||
|
||||
public sealed class QBitService : IDownloadService
|
||||
public sealed class QBitService : DownloadServiceBase
|
||||
{
|
||||
private readonly ILogger<QBitService> _logger;
|
||||
private readonly QBitConfig _config;
|
||||
private readonly QBittorrentClient _client;
|
||||
private readonly FilenameEvaluator _filenameEvaluator;
|
||||
|
||||
public QBitService(
|
||||
ILogger<QBitService> logger,
|
||||
IOptions<QBitConfig> config,
|
||||
FilenameEvaluator filenameEvaluator
|
||||
)
|
||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||
FilenameEvaluator filenameEvaluator,
|
||||
Striker striker
|
||||
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_config.Validate();
|
||||
_client = new(_config.Url);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
public override async Task LoginAsync()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_config.Username) && string.IsNullOrEmpty(_config.Password))
|
||||
{
|
||||
@@ -37,13 +36,14 @@ public sealed class QBitService : IDownloadService
|
||||
await _client.LoginAsync(_config.Username, _config.Password);
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] }))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (torrent is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -55,17 +55,16 @@ public sealed class QBitService : IDownloadService
|
||||
|
||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||
|
||||
// if no files found, torrent might be stuck in Downloading metadata
|
||||
if (files?.Count is null or 0)
|
||||
// if all files are marked as skip
|
||||
if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// if all files are marked as skip
|
||||
return files.All(x => x.Priority is TorrentContentPriority.Skip);
|
||||
return IsItemStuckAndShouldRemove(torrent);
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash);
|
||||
|
||||
@@ -91,8 +90,20 @@ public sealed class QBitService : IDownloadService
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public override void Dispose()
|
||||
{
|
||||
_client.Dispose();
|
||||
}
|
||||
|
||||
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
|
||||
{
|
||||
if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata
|
||||
and not TorrentState.ForcedFetchingMetadata)
|
||||
{
|
||||
// ignore other states
|
||||
return false;
|
||||
}
|
||||
|
||||
return StrikeAndCheckLimit(torrent.Hash, torrent.Name);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Common.Configuration;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.DownloadClient;
|
||||
using Common.Configuration.QueueCleaner;
|
||||
using Infrastructure.Verticals.ContentBlocker;
|
||||
using Infrastructure.Verticals.ItemStriker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Transmission.API.RPC;
|
||||
@@ -9,21 +10,20 @@ using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Infrastructure.Verticals.DownloadClient.Transmission;
|
||||
|
||||
public sealed class TransmissionService : IDownloadService
|
||||
public sealed class TransmissionService : DownloadServiceBase
|
||||
{
|
||||
private readonly ILogger<TransmissionService> _logger;
|
||||
private readonly TransmissionConfig _config;
|
||||
private readonly Client _client;
|
||||
private readonly FilenameEvaluator _filenameEvaluator;
|
||||
private TorrentInfo[]? _torrentsCache;
|
||||
|
||||
public TransmissionService(
|
||||
ILogger<TransmissionService> logger,
|
||||
IOptions<TransmissionConfig> config,
|
||||
FilenameEvaluator filenameEvaluator
|
||||
)
|
||||
IOptions<QueueCleanerConfig> queueCleanerConfig,
|
||||
FilenameEvaluator filenameEvaluator,
|
||||
Striker striker
|
||||
) : base(logger, queueCleanerConfig, filenameEvaluator, striker)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_config.Validate();
|
||||
_client = new(
|
||||
@@ -31,44 +31,45 @@ public sealed class TransmissionService : IDownloadService
|
||||
login: _config.Username,
|
||||
password: _config.Password
|
||||
);
|
||||
_filenameEvaluator = filenameEvaluator;
|
||||
}
|
||||
|
||||
public async Task LoginAsync()
|
||||
public override async Task LoginAsync()
|
||||
{
|
||||
await _client.GetSessionInformationAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
public override async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
|
||||
// if no files found, torrent might be stuck in Downloading metadata
|
||||
if (torrent?.FileStats?.Length is null or 0)
|
||||
if (torrent is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool shouldRemove = torrent.FileStats?.Length > 0;
|
||||
|
||||
foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? [])
|
||||
{
|
||||
if (!stats.Wanted.HasValue)
|
||||
{
|
||||
// if any files stats are missing, do not remove
|
||||
return false;
|
||||
shouldRemove = false;
|
||||
}
|
||||
|
||||
if (stats.Wanted.HasValue && stats.Wanted.Value)
|
||||
{
|
||||
// if any files are wanted, do not remove
|
||||
return false;
|
||||
shouldRemove = false;
|
||||
}
|
||||
}
|
||||
|
||||
// remove if all files are unwanted
|
||||
return true;
|
||||
return shouldRemove || IsItemStuckAndShouldRemove(torrent);
|
||||
}
|
||||
|
||||
public async Task BlockUnwantedFilesAsync(string hash)
|
||||
public override async Task BlockUnwantedFilesAsync(string hash)
|
||||
{
|
||||
TorrentInfo? torrent = await GetTorrentAsync(hash);
|
||||
|
||||
@@ -108,10 +109,26 @@ public sealed class TransmissionService : IDownloadService
|
||||
FilesUnwanted = unwantedFiles.ToArray(),
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
|
||||
{
|
||||
if (torrent.Status is not 4)
|
||||
{
|
||||
// not in downloading state
|
||||
return false;
|
||||
}
|
||||
|
||||
if (torrent.Eta > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return StrikeAndCheckLimit(torrent.HashString!, torrent.Name!);
|
||||
}
|
||||
|
||||
private async Task<TorrentInfo?> GetTorrentAsync(string hash)
|
||||
{
|
||||
@@ -120,7 +137,15 @@ public sealed class TransmissionService : IDownloadService
|
||||
|
||||
if (_torrentsCache is null || torrent is null)
|
||||
{
|
||||
string[] fields = [TorrentFields.FILES, TorrentFields.FILE_STATS, TorrentFields.HASH_STRING, TorrentFields.ID];
|
||||
string[] fields = [
|
||||
TorrentFields.FILES,
|
||||
TorrentFields.FILE_STATS,
|
||||
TorrentFields.HASH_STRING,
|
||||
TorrentFields.ID,
|
||||
TorrentFields.ETA,
|
||||
TorrentFields.NAME,
|
||||
TorrentFields.STATUS
|
||||
];
|
||||
|
||||
// refresh cache
|
||||
_torrentsCache = (await _client.TorrentGetAsync(fields))
|
||||
|
||||
Reference in New Issue
Block a user