Files
Cleanuparr/code/backend/Cleanuparr.Infrastructure/Services/RuleEvaluator.cs
2026-02-14 04:00:05 +02:00

235 lines
7.6 KiB
C#

using Cleanuparr.Domain.Entities;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Infrastructure.Features.DownloadClient;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Services.Interfaces;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Cleanuparr.Persistence.Models.State;
using Cleanuparr.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Services;
public class RuleEvaluator : IRuleEvaluator
{
private readonly IRuleManager _ruleManager;
private readonly IStriker _striker;
private readonly EventsContext _context;
private readonly ILogger<RuleEvaluator> _logger;
public RuleEvaluator(
IRuleManager ruleManager,
IStriker striker,
EventsContext context,
ILogger<RuleEvaluator> logger)
{
_ruleManager = ruleManager;
_striker = striker;
_context = context;
_logger = logger;
}
public async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateStallRulesAsync(ITorrentItemWrapper torrent)
{
_logger.LogTrace("Evaluating stall rules | {name}", torrent.Name);
var rule = _ruleManager.GetMatchingStallRule(torrent);
if (rule is null)
{
_logger.LogTrace("skip | no stall rules matched | {name}", torrent.Name);
return (false, DeleteReason.None, false);
}
_logger.LogTrace("Applying stall rule {rule} | {name}", rule.Name, torrent.Name);
ContextProvider.Set<QueueRule>(rule);
await ResetStalledStrikesAsync(
torrent,
rule.ResetStrikesOnProgress,
rule.MinimumProgressByteSize?.Bytes
);
long currentDownloaded = Math.Max(0, torrent.DownloadedBytes);
bool shouldRemove = await _striker.StrikeAndCheckLimit(
torrent.Hash,
torrent.Name,
(ushort)rule.MaxStrikes,
StrikeType.Stalled,
currentDownloaded
);
if (shouldRemove)
{
return (true, DeleteReason.Stalled, rule.DeletePrivateTorrentsFromClient);
}
return (false, DeleteReason.None, false);
}
public async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateSlowRulesAsync(ITorrentItemWrapper torrent)
{
_logger.LogTrace("Evaluating slow rules | {name}", torrent.Name);
SlowRule? rule = _ruleManager.GetMatchingSlowRule(torrent);
if (rule is null)
{
_logger.LogDebug("skip | no slow rules matched | {name}", torrent.Name);
return (false, DeleteReason.None, false);
}
_logger.LogTrace("Applying slow rule {rule} | {name}", rule.Name, torrent.Name);
ContextProvider.Set<QueueRule>(rule);
if (!string.IsNullOrWhiteSpace(rule.MinSpeed))
{
ByteSize minSpeed = rule.MinSpeedByteSize;
ByteSize currentSpeed = new ByteSize(torrent.DownloadSpeed);
if (currentSpeed.Bytes < minSpeed.Bytes)
{
bool shouldRemove = await _striker.StrikeAndCheckLimit(
torrent.Hash,
torrent.Name,
(ushort)rule.MaxStrikes,
StrikeType.SlowSpeed
);
if (shouldRemove)
{
return (true, DeleteReason.SlowSpeed, rule.DeletePrivateTorrentsFromClient);
}
}
else
{
await ResetSlowStrikesAsync(torrent, rule.ResetStrikesOnProgress, StrikeType.SlowSpeed);
}
}
if (rule.MaxTimeHours > 0)
{
SmartTimeSpan maxTime = SmartTimeSpan.FromHours(rule.MaxTimeHours);
SmartTimeSpan currentTime = SmartTimeSpan.FromSeconds(torrent.Eta);
if (currentTime.Time.TotalSeconds > maxTime.Time.TotalSeconds && maxTime.Time.TotalSeconds > 0)
{
bool shouldRemove = await _striker.StrikeAndCheckLimit(
torrent.Hash,
torrent.Name,
(ushort)rule.MaxStrikes,
StrikeType.SlowTime
);
if (shouldRemove)
{
return (true, DeleteReason.SlowTime, rule.DeletePrivateTorrentsFromClient);
}
}
else
{
await ResetSlowStrikesAsync(torrent, rule.ResetStrikesOnProgress, StrikeType.SlowTime);
}
}
return (false, DeleteReason.None, false);
}
private async Task ResetStalledStrikesAsync(
ITorrentItemWrapper torrent,
bool resetEnabled,
long? minimumProgressBytes
)
{
if (!resetEnabled)
{
return;
}
var (hasProgress, previous, current) = await GetDownloadProgressAsync(torrent);
if (!hasProgress)
{
_logger.LogTrace("No progress detected | strikes are not reset | {name}", torrent.Name);
return;
}
long progressBytes = current - previous;
if (minimumProgressBytes is > 0)
{
if (progressBytes < minimumProgressBytes)
{
_logger.LogTrace(
"Progress detected | strikes are not reset | progress: {progress}b | minimum: {minimum}b | {name}",
progressBytes,
minimumProgressBytes,
torrent.Name
);
return;
}
_logger.LogTrace(
"Progress detected | strikes are reset | progress: {progress}b | minimum: {minimum}b | {name}",
progressBytes,
minimumProgressBytes,
torrent.Name
);
}
else
{
_logger.LogTrace(
"Progress detected | strikes are reset | progress: {progress}b | {name}",
progressBytes,
torrent.Name
);
}
await _striker.ResetStrikeAsync(torrent.Hash, torrent.Name, StrikeType.Stalled);
}
private async Task ResetSlowStrikesAsync(
ITorrentItemWrapper torrent,
bool resetEnabled,
StrikeType strikeType
)
{
if (!resetEnabled)
{
return;
}
await _striker.ResetStrikeAsync(torrent.Hash, torrent.Name, strikeType);
}
private async Task<(bool HasProgress, long PreviousDownloaded, long CurrentDownloaded)> GetDownloadProgressAsync(ITorrentItemWrapper torrent)
{
long currentDownloaded = Math.Max(0, torrent.DownloadedBytes);
var downloadItem = await _context.DownloadItems
.FirstOrDefaultAsync(d => d.DownloadId == torrent.Hash);
if (downloadItem is null)
{
return (false, 0, currentDownloaded);
}
// Get the most recent strike for this download item (Stalled type) to check progress
var mostRecentStrike = await _context.Strikes
.Where(s => s.DownloadItemId == downloadItem.Id && s.Type == StrikeType.Stalled)
.OrderByDescending(s => s.CreatedAt)
.FirstOrDefaultAsync();
if (mostRecentStrike is null)
{
return (false, 0, currentDownloaded);
}
long previousDownloaded = mostRecentStrike.LastDownloadedBytes ?? 0;
bool progressed = currentDownloaded > previousDownloaded;
return (progressed, previousDownloaded, currentDownloaded);
}
}