diff --git a/code/Domain/Enums/DeleteReason.cs b/code/Domain/Enums/DeleteReason.cs index e46cdd6..baeddd3 100644 --- a/code/Domain/Enums/DeleteReason.cs +++ b/code/Domain/Enums/DeleteReason.cs @@ -2,7 +2,11 @@ public enum DeleteReason { + None, Stalled, ImportFailed, - AllFilesBlocked + DownloadingMetadata, + AllFilesSkipped, + AllFilesSkippedByQBit, + AllFilesBlocked, } \ No newline at end of file diff --git a/code/Domain/Enums/StrikeType.cs b/code/Domain/Enums/StrikeType.cs index 1a04c15..234401d 100644 --- a/code/Domain/Enums/StrikeType.cs +++ b/code/Domain/Enums/StrikeType.cs @@ -3,5 +3,6 @@ public enum StrikeType { Stalled, + DownloadingMetadata, ImportFailed } \ No newline at end of file diff --git a/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceTests.cs b/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceTests.cs index 9fcba54..d63fd3d 100644 --- a/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceTests.cs +++ b/code/Infrastructure.Tests/Verticals/DownloadClient/DownloadServiceTests.cs @@ -105,13 +105,14 @@ public class DownloadServiceTests : IClassFixture // Arrange const string hash = "test-hash"; const string itemName = "test-item"; - _fixture.Striker.StrikeAndCheckLimit(hash, itemName, 3, StrikeType.Stalled) + StrikeType strikeType = StrikeType.Stalled; + _fixture.Striker.StrikeAndCheckLimit(hash, itemName, 3, strikeType) .Returns(true); TestDownloadService sut = _fixture.CreateSut(); // Act - bool result = await sut.StrikeAndCheckLimit(hash, itemName); + bool result = await sut.StrikeAndCheckLimit(hash, itemName, strikeType); // Assert result.ShouldBeTrue(); diff --git a/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs b/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs index 60029be..fc0a125 100644 --- a/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs +++ b/code/Infrastructure.Tests/Verticals/DownloadClient/TestDownloadService.cs @@ -3,6 +3,7 @@ using System.Text.RegularExpressions; using Common.Configuration.ContentBlocker; using Common.Configuration.DownloadCleaner; using Common.Configuration.QueueCleaner; +using Domain.Enums; using Infrastructure.Interceptors; using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.DownloadClient; @@ -45,6 +46,6 @@ public class TestDownloadService : DownloadService // Expose protected methods for testing public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded); - public new Task StrikeAndCheckLimit(string hash, string itemName) => base.StrikeAndCheckLimit(hash, itemName); + public new Task StrikeAndCheckLimit(string hash, string itemName, StrikeType strikeType) => base.StrikeAndCheckLimit(hash, itemName, strikeType); public new SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category) => base.ShouldCleanDownload(ratio, seedingTime, category); } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Arr/ArrClient.cs b/code/Infrastructure/Verticals/Arr/ArrClient.cs index d5d87ce..3574ed1 100644 --- a/code/Infrastructure/Verticals/Arr/ArrClient.cs +++ b/code/Infrastructure/Verticals/Arr/ArrClient.cs @@ -114,7 +114,12 @@ public abstract class ArrClient : IArrClient return false; } - public virtual async Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient) + public virtual async Task DeleteQueueItemAsync( + ArrInstance arrInstance, + QueueRecord record, + bool removeFromClient, + DeleteReason deleteReason + ) { UriBuilder uriBuilder = new(arrInstance.Url); uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/{GetQueueDeleteUrlPath(record.Id).TrimStart('/')}"; @@ -130,8 +135,9 @@ public abstract class ArrClient : IArrClient _logger.LogInformation( removeFromClient - ? "queue item deleted | {url} | {title}" - : "queue item removed from arr | {url} | {title}", + ? "queue item deleted with reason {reason} | {url} | {title}" + : "queue item removed from arr with reason {reason} | {url} | {title}", + deleteReason.ToString(), arrInstance.Url, record.Title ); diff --git a/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs b/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs index 4435a05..30027a9 100644 --- a/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs +++ b/code/Infrastructure/Verticals/Arr/Interfaces/IArrClient.cs @@ -11,7 +11,7 @@ public interface IArrClient Task ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload); - Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient); + Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, DeleteReason deleteReason); Task RefreshItemsAsync(ArrInstance arrInstance, HashSet? items); diff --git a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs index 07ee792..a998180 100644 --- a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs +++ b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs @@ -142,7 +142,7 @@ public sealed class ContentBlocker : GenericHandler removeFromClient = false; } - await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient); + await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient, DeleteReason.AllFilesBlocked); await _notifier.NotifyQueueItemDeleted(removeFromClient, DeleteReason.AllFilesBlocked); } }); diff --git a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs index d104b04..689518e 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs @@ -65,6 +65,8 @@ public class DelugeService : DownloadService, IDelugeService return result; } + result.IsPrivate = download.Private; + if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads)) { _logger.LogInformation("skip | download is ignored | {name}", download.Name); @@ -79,6 +81,7 @@ public class DelugeService : DownloadService, IDelugeService { _logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash); } + bool shouldRemove = contents?.Contents?.Count > 0; @@ -92,17 +95,15 @@ public class DelugeService : DownloadService, IDelugeService if (shouldRemove) { - result.DeleteReason = DeleteReason.AllFilesBlocked; + // remove if all files are unwanted + result.ShouldRemove = true; + result.DeleteReason = DeleteReason.AllFilesSkipped; + return result; } - result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download); - result.IsPrivate = download.Private; + // remove if download is stuck + (result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download); - if (!shouldRemove && result.ShouldRemove) - { - result.DeleteReason = DeleteReason.Stalled; - } - return result; } @@ -295,33 +296,33 @@ public class DelugeService : DownloadService, IDelugeService await _client.ChangeFilesPriority(hash, sortedPriorities); } - private async Task IsItemStuckAndShouldRemove(TorrentStatus status) + private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentStatus status) { if (_queueCleanerConfig.StalledMaxStrikes is 0) { - return false; + return (false, default); } if (_queueCleanerConfig.StalledIgnorePrivate && status.Private) { // ignore private trackers _logger.LogDebug("skip stalled check | download is private | {name}", status.Name); - return false; + return (false, default); } if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase)) { - return false; + return (false, default); } if (status.Eta > 0) { - return false; + return (false, default); } ResetStrikesOnProgress(status.Hash!, status.TotalDone); - return await StrikeAndCheckLimit(status.Hash!, status.Name!); + return (await StrikeAndCheckLimit(status.Hash!, status.Name!, StrikeType.Stalled), DeleteReason.Stalled); } private static void ProcessFiles(Dictionary? contents, Action processFile) diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs index 587fd53..7b703bc 100644 --- a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs @@ -100,10 +100,11 @@ public abstract class DownloadService : IDownloadService /// /// The torrent hash. /// The name or title of the item. + /// /// True if the limit has been reached; otherwise, false. - protected async Task StrikeAndCheckLimit(string hash, string itemName) + protected async Task StrikeAndCheckLimit(string hash, string itemName, StrikeType strikeType) { - return await _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled); + return await _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, strikeType); } protected SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category) diff --git a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs index 4d55d7c..34c27d4 100644 --- a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs @@ -96,30 +96,26 @@ public class QBitService : DownloadService, IQBitService bool.TryParse(dictValue?.ToString(), out bool boolValue) && boolValue; - // if all files were blocked by qBittorrent - if (download is { CompletionOn: not null, Downloaded: null or 0 }) - { - result.ShouldRemove = true; - result.DeleteReason = DeleteReason.AllFilesBlocked; - return result; - } - IReadOnlyList? files = await _client.GetTorrentContentsAsync(hash); - // if all files are marked as skip if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip)) { result.ShouldRemove = true; - result.DeleteReason = DeleteReason.AllFilesBlocked; + + // if all files were blocked by qBittorrent + if (download is { CompletionOn: not null, Downloaded: null or 0 }) + { + result.DeleteReason = DeleteReason.AllFilesSkippedByQBit; + return result; + } + + // remove if all files are unwanted + result.DeleteReason = DeleteReason.AllFilesSkipped; return result; } - result.ShouldRemove = await IsItemStuckAndShouldRemove(download, result.IsPrivate); - - if (result.ShouldRemove) - { - result.DeleteReason = DeleteReason.Stalled; - } + // remove if download is stuck + (result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download, result.IsPrivate); return result; } @@ -337,30 +333,35 @@ public class QBitService : DownloadService, IQBitService _client.Dispose(); } - private async Task IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate) + private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate) { if (_queueCleanerConfig.StalledMaxStrikes is 0) { - return false; + return (false, default); } if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate) { // ignore private trackers _logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name); - return false; + return (false, default); } if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata and not TorrentState.ForcedFetchingMetadata) { // ignore other states - return false; + return (false, default); } ResetStrikesOnProgress(torrent.Hash, torrent.Downloaded ?? 0); - return await StrikeAndCheckLimit(torrent.Hash, torrent.Name); + if (torrent.State is TorrentState.StalledDownload) + { + return (await StrikeAndCheckLimit(torrent.Hash, torrent.Name, StrikeType.Stalled), DeleteReason.Stalled); + } + + return (await StrikeAndCheckLimit(torrent.Hash, torrent.Name, StrikeType.DownloadingMetadata), DeleteReason.DownloadingMetadata); } private async Task> GetTrackersAsync(string hash) diff --git a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs index 7348959..0503ca8 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs @@ -119,17 +119,15 @@ public class TransmissionService : DownloadService, ITransmissionService if (shouldRemove) { + // remove if all files are unwanted + result.ShouldRemove = true; result.DeleteReason = DeleteReason.AllFilesBlocked; + return result; } - // remove if all files are unwanted or download is stuck - result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download); + // remove if download is stuck + (result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download); - if (!shouldRemove && result.ShouldRemove) - { - result.DeleteReason = DeleteReason.Stalled; - } - return result; } @@ -338,34 +336,34 @@ public class TransmissionService : DownloadService, ITransmissionService }); } - private async Task IsItemStuckAndShouldRemove(TorrentInfo torrent) + private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentInfo torrent) { if (_queueCleanerConfig.StalledMaxStrikes is 0) { - return false; + return (false, default); } if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false)) { // ignore private trackers _logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name); - return false; + return (false, default); } if (torrent.Status is not 4) { // not in downloading state - return false; + return (false, default); } if (torrent.Eta > 0) { - return false; + return (false, default); } ResetStrikesOnProgress(torrent.HashString!, torrent.DownloadedEver ?? 0); - return await StrikeAndCheckLimit(torrent.HashString!, torrent.Name!); + return (await StrikeAndCheckLimit(torrent.HashString!, torrent.Name!, StrikeType.Stalled), DeleteReason.Stalled); } private async Task GetTorrentAsync(string hash) diff --git a/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs b/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs index 3991407..32aadca 100644 --- a/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs +++ b/code/Infrastructure/Verticals/QueueCleaner/QueueCleaner.cs @@ -132,7 +132,7 @@ public sealed class QueueCleaner : GenericHandler } } - await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient); + await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient, deleteReason); await _notifier.NotifyQueueItemDeleted(removeFromClient, deleteReason); } });