Compare commits

...

2 Commits

Author SHA1 Message Date
Flaminel 9f770473e5 Remove Transmission downloads cache (#105) 2025-03-26 00:26:10 +02:00
Flaminel 5fe0f5750a Fix qBit queued items being processed (#102) 2025-03-21 23:06:31 +02:00
13 changed files with 79 additions and 88 deletions
+5 -1
View File
@@ -2,7 +2,11 @@
public enum DeleteReason public enum DeleteReason
{ {
None,
Stalled, Stalled,
ImportFailed, ImportFailed,
AllFilesBlocked DownloadingMetadata,
AllFilesSkipped,
AllFilesSkippedByQBit,
AllFilesBlocked,
} }
+1
View File
@@ -3,5 +3,6 @@
public enum StrikeType public enum StrikeType
{ {
Stalled, Stalled,
DownloadingMetadata,
ImportFailed ImportFailed
} }
@@ -105,13 +105,14 @@ public class DownloadServiceTests : IClassFixture<DownloadServiceFixture>
// Arrange // Arrange
const string hash = "test-hash"; const string hash = "test-hash";
const string itemName = "test-item"; 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); .Returns(true);
TestDownloadService sut = _fixture.CreateSut(); TestDownloadService sut = _fixture.CreateSut();
// Act // Act
bool result = await sut.StrikeAndCheckLimit(hash, itemName); bool result = await sut.StrikeAndCheckLimit(hash, itemName, strikeType);
// Assert // Assert
result.ShouldBeTrue(); result.ShouldBeTrue();
@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using Common.Configuration.ContentBlocker; using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadCleaner; using Common.Configuration.DownloadCleaner;
using Common.Configuration.QueueCleaner; using Common.Configuration.QueueCleaner;
using Domain.Enums;
using Infrastructure.Interceptors; using Infrastructure.Interceptors;
using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.DownloadClient; using Infrastructure.Verticals.DownloadClient;
@@ -45,6 +46,6 @@ public class TestDownloadService : DownloadService
// Expose protected methods for testing // Expose protected methods for testing
public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded); public new void ResetStrikesOnProgress(string hash, long downloaded) => base.ResetStrikesOnProgress(hash, downloaded);
public new Task<bool> StrikeAndCheckLimit(string hash, string itemName) => base.StrikeAndCheckLimit(hash, itemName); public new Task<bool> 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); public new SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category) => base.ShouldCleanDownload(ratio, seedingTime, category);
} }
+1 -1
View File
@@ -13,7 +13,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FLM.QBittorrent" Version="1.0.0" /> <PackageReference Include="FLM.QBittorrent" Version="1.0.0" />
<PackageReference Include="FLM.Transmission" Version="1.0.2" /> <PackageReference Include="FLM.Transmission" Version="1.0.3" />
<PackageReference Include="Mapster" Version="7.4.0" /> <PackageReference Include="Mapster" Version="7.4.0" />
<PackageReference Include="MassTransit" Version="8.3.6" /> <PackageReference Include="MassTransit" Version="8.3.6" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
@@ -114,7 +114,12 @@ public abstract class ArrClient : IArrClient
return false; 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 uriBuilder = new(arrInstance.Url);
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/{GetQueueDeleteUrlPath(record.Id).TrimStart('/')}"; uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/{GetQueueDeleteUrlPath(record.Id).TrimStart('/')}";
@@ -130,8 +135,9 @@ public abstract class ArrClient : IArrClient
_logger.LogInformation( _logger.LogInformation(
removeFromClient removeFromClient
? "queue item deleted | {url} | {title}" ? "queue item deleted with reason {reason} | {url} | {title}"
: "queue item removed from arr | {url} | {title}", : "queue item removed from arr with reason {reason} | {url} | {title}",
deleteReason.ToString(),
arrInstance.Url, arrInstance.Url,
record.Title record.Title
); );
@@ -11,7 +11,7 @@ public interface IArrClient
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload); Task<bool> 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<SearchItem>? items); Task RefreshItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
@@ -142,7 +142,7 @@ public sealed class ContentBlocker : GenericHandler
removeFromClient = false; removeFromClient = false;
} }
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient); await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient, DeleteReason.AllFilesBlocked);
await _notifier.NotifyQueueItemDeleted(removeFromClient, DeleteReason.AllFilesBlocked); await _notifier.NotifyQueueItemDeleted(removeFromClient, DeleteReason.AllFilesBlocked);
} }
}); });
@@ -65,6 +65,8 @@ public class DelugeService : DownloadService, IDelugeService
return result; return result;
} }
result.IsPrivate = download.Private;
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads)) if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
{ {
_logger.LogInformation("skip | download is ignored | {name}", download.Name); _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); _logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
} }
bool shouldRemove = contents?.Contents?.Count > 0; bool shouldRemove = contents?.Contents?.Count > 0;
@@ -92,17 +95,15 @@ public class DelugeService : DownloadService, IDelugeService
if (shouldRemove) 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); // remove if download is stuck
result.IsPrivate = download.Private; (result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download);
if (!shouldRemove && result.ShouldRemove)
{
result.DeleteReason = DeleteReason.Stalled;
}
return result; return result;
} }
@@ -295,33 +296,33 @@ public class DelugeService : DownloadService, IDelugeService
await _client.ChangeFilesPriority(hash, sortedPriorities); await _client.ChangeFilesPriority(hash, sortedPriorities);
} }
private async Task<bool> IsItemStuckAndShouldRemove(TorrentStatus status) private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentStatus status)
{ {
if (_queueCleanerConfig.StalledMaxStrikes is 0) if (_queueCleanerConfig.StalledMaxStrikes is 0)
{ {
return false; return (false, default);
} }
if (_queueCleanerConfig.StalledIgnorePrivate && status.Private) if (_queueCleanerConfig.StalledIgnorePrivate && status.Private)
{ {
// ignore private trackers // ignore private trackers
_logger.LogDebug("skip stalled check | download is private | {name}", status.Name); _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)) if (status.State is null || !status.State.Equals("Downloading", StringComparison.InvariantCultureIgnoreCase))
{ {
return false; return (false, default);
} }
if (status.Eta > 0) if (status.Eta > 0)
{ {
return false; return (false, default);
} }
ResetStrikesOnProgress(status.Hash!, status.TotalDone); 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<string, DelugeFileOrDirectory>? contents, Action<string, DelugeFileOrDirectory> processFile) private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory>? contents, Action<string, DelugeFileOrDirectory> processFile)
@@ -100,10 +100,11 @@ public abstract class DownloadService : IDownloadService
/// </summary> /// </summary>
/// <param name="hash">The torrent hash.</param> /// <param name="hash">The torrent hash.</param>
/// <param name="itemName">The name or title of the item.</param> /// <param name="itemName">The name or title of the item.</param>
/// <param name="strikeType"></param>
/// <returns>True if the limit has been reached; otherwise, false.</returns> /// <returns>True if the limit has been reached; otherwise, false.</returns>
protected async Task<bool> StrikeAndCheckLimit(string hash, string itemName) protected async Task<bool> 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) protected SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, Category category)
@@ -96,30 +96,26 @@ public class QBitService : DownloadService, IQBitService
bool.TryParse(dictValue?.ToString(), out bool boolValue) bool.TryParse(dictValue?.ToString(), out bool boolValue)
&& 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<TorrentContent>? files = await _client.GetTorrentContentsAsync(hash); IReadOnlyList<TorrentContent>? 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)) if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
{ {
result.ShouldRemove = true; 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; return result;
} }
result.ShouldRemove = await IsItemStuckAndShouldRemove(download, result.IsPrivate); // remove if download is stuck
(result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download, result.IsPrivate);
if (result.ShouldRemove)
{
result.DeleteReason = DeleteReason.Stalled;
}
return result; return result;
} }
@@ -337,30 +333,35 @@ public class QBitService : DownloadService, IQBitService
_client.Dispose(); _client.Dispose();
} }
private async Task<bool> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate) private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
{ {
if (_queueCleanerConfig.StalledMaxStrikes is 0) if (_queueCleanerConfig.StalledMaxStrikes is 0)
{ {
return false; return (false, default);
} }
if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate) if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate)
{ {
// ignore private trackers // ignore private trackers
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name); _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 if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata
and not TorrentState.ForcedFetchingMetadata) and not TorrentState.ForcedFetchingMetadata)
{ {
// ignore other states // ignore other states
return false; return (false, default);
} }
ResetStrikesOnProgress(torrent.Hash, torrent.Downloaded ?? 0); 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<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash) private async Task<IReadOnlyList<TorrentTracker>> GetTrackersAsync(string hash)
@@ -26,7 +26,6 @@ public class TransmissionService : DownloadService, ITransmissionService
{ {
private readonly TransmissionConfig _config; private readonly TransmissionConfig _config;
private readonly Client _client; private readonly Client _client;
private TorrentInfo[]? _torrentsCache;
private static readonly string[] Fields = private static readonly string[] Fields =
[ [
@@ -119,17 +118,15 @@ public class TransmissionService : DownloadService, ITransmissionService
if (shouldRemove) if (shouldRemove)
{ {
// remove if all files are unwanted
result.ShouldRemove = true;
result.DeleteReason = DeleteReason.AllFilesBlocked; result.DeleteReason = DeleteReason.AllFilesBlocked;
return result;
} }
// remove if all files are unwanted or download is stuck // remove if download is stuck
result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(download); (result.ShouldRemove, result.DeleteReason) = await IsItemStuckAndShouldRemove(download);
if (!shouldRemove && result.ShouldRemove)
{
result.DeleteReason = DeleteReason.Stalled;
}
return result; return result;
} }
@@ -338,60 +335,38 @@ public class TransmissionService : DownloadService, ITransmissionService
}); });
} }
private async Task<bool> IsItemStuckAndShouldRemove(TorrentInfo torrent) private async Task<(bool, DeleteReason)> IsItemStuckAndShouldRemove(TorrentInfo torrent)
{ {
if (_queueCleanerConfig.StalledMaxStrikes is 0) if (_queueCleanerConfig.StalledMaxStrikes is 0)
{ {
return false; return (false, default);
} }
if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false)) if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false))
{ {
// ignore private trackers // ignore private trackers
_logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name); _logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name);
return false; return (false, default);
} }
if (torrent.Status is not 4) if (torrent.Status is not 4)
{ {
// not in downloading state // not in downloading state
return false; return (false, default);
} }
if (torrent.Eta > 0) if (torrent.Eta > 0)
{ {
return false; return (false, default);
} }
ResetStrikesOnProgress(torrent.HashString!, torrent.DownloadedEver ?? 0); 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<TorrentInfo?> GetTorrentAsync(string hash) private async Task<TorrentInfo?> GetTorrentAsync(string hash) =>
{ (await _client.TorrentGetAsync(Fields, hash))
TorrentInfo? torrent = _torrentsCache? ?.Torrents
.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase)); ?.FirstOrDefault();
if (_torrentsCache is null || torrent is null)
{
// refresh cache
_torrentsCache = (await _client.TorrentGetAsync(Fields))
?.Torrents;
}
if (_torrentsCache?.Length is null or 0)
{
_logger.LogDebug("could not list torrents | {url}", _config.Url);
}
torrent = _torrentsCache?.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
if (torrent is null)
{
_logger.LogDebug("could not find torrent | {hash} | {url}", hash, _config.Url);
}
return torrent;
}
} }
@@ -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); await _notifier.NotifyQueueItemDeleted(removeFromClient, deleteReason);
} }
}); });