mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-05-19 03:55:52 -04:00
Add option to change the category instead of deleting queue items (#602)
This commit is contained in:
@@ -24,4 +24,6 @@ public abstract record QueueRuleDto
|
||||
public ushort MaxCompletionPercentage { get; set; }
|
||||
|
||||
public bool DeletePrivateTorrentsFromClient { get; set; } = false;
|
||||
|
||||
public bool ChangeCategory { get; set; } = false;
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ public class QueueRulesController : ControllerBase
|
||||
MaxCompletionPercentage = ruleDto.MaxCompletionPercentage,
|
||||
ResetStrikesOnProgress = ruleDto.ResetStrikesOnProgress,
|
||||
DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient,
|
||||
ChangeCategory = ruleDto.ChangeCategory,
|
||||
MinimumProgress = ruleDto.MinimumProgress?.Trim(),
|
||||
};
|
||||
|
||||
@@ -161,6 +162,7 @@ public class QueueRulesController : ControllerBase
|
||||
MaxCompletionPercentage = ruleDto.MaxCompletionPercentage,
|
||||
ResetStrikesOnProgress = ruleDto.ResetStrikesOnProgress,
|
||||
DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient,
|
||||
ChangeCategory = ruleDto.ChangeCategory,
|
||||
MinimumProgress = ruleDto.MinimumProgress?.Trim(),
|
||||
};
|
||||
|
||||
@@ -293,6 +295,7 @@ public class QueueRulesController : ControllerBase
|
||||
MaxTimeHours = ruleDto.MaxTimeHours,
|
||||
IgnoreAboveSize = ruleDto.IgnoreAboveSize,
|
||||
DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient,
|
||||
ChangeCategory = ruleDto.ChangeCategory,
|
||||
};
|
||||
|
||||
var existingRules = await _dataContext.SlowRules.ToListAsync();
|
||||
@@ -368,6 +371,7 @@ public class QueueRulesController : ControllerBase
|
||||
MaxTimeHours = ruleDto.MaxTimeHours,
|
||||
IgnoreAboveSize = ruleDto.IgnoreAboveSize,
|
||||
DeletePrivateTorrentsFromClient = ruleDto.DeletePrivateTorrentsFromClient,
|
||||
ChangeCategory = ruleDto.ChangeCategory,
|
||||
};
|
||||
|
||||
var existingRules = await _dataContext.SlowRules
|
||||
|
||||
@@ -74,11 +74,11 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -119,11 +119,11 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -211,11 +211,11 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -356,7 +356,7 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -397,7 +397,7 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -445,13 +445,14 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
.Returns((true, DeleteReason.SlowSpeed, true, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -488,13 +489,57 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
.Returns((true, DeleteReason.Stalled, true, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
|
||||
{
|
||||
const string hash = "test-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var downloadStatus = new DownloadStatus
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Test Torrent",
|
||||
State = DelugeState.Downloading,
|
||||
Private = false,
|
||||
DownloadSpeed = 1000,
|
||||
Trackers = new List<Tracker>(),
|
||||
DownloadLocation = "/downloads"
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "file1.mkv", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0 } }
|
||||
}
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<DelugeItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, false, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,11 +135,11 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -193,11 +193,11 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -401,11 +401,11 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -654,7 +654,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -709,7 +709,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -764,7 +764,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true)); // Rule matched
|
||||
.Returns((true, DeleteReason.SlowSpeed, true, false)); // Rule matched
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -773,6 +773,63 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
|
||||
{
|
||||
// Arrange
|
||||
const string hash = "test-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var torrentInfo = new TorrentInfo
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Test Torrent",
|
||||
State = TorrentState.Downloading,
|
||||
DownloadSpeed = 1000
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
AdditionalData = new Dictionary<string, JToken>
|
||||
{
|
||||
{ "is_private", JToken.FromObject(false) }
|
||||
}
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, false, true));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,7 +883,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -880,7 +937,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true)); // Rule matched
|
||||
.Returns((true, DeleteReason.Stalled, true, false)); // Rule matched
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -889,6 +946,62 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StalledDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
|
||||
{
|
||||
// Arrange
|
||||
const string hash = "test-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var torrentInfo = new TorrentInfo
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Test Torrent",
|
||||
State = TorrentState.StalledDownload
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentListAsync(Arg.Is<TorrentListQuery>(q => q.Hashes != null && q.Hashes.Contains(hash)))
|
||||
.Returns(new[] { torrentInfo });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentTrackersAsync(hash)
|
||||
.Returns(Array.Empty<TorrentTracker>());
|
||||
|
||||
var properties = new TorrentProperties
|
||||
{
|
||||
AdditionalData = new Dictionary<string, JToken>
|
||||
{
|
||||
{ "is_private", JToken.FromObject(false) }
|
||||
}
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(properties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentContentsAsync(hash)
|
||||
.Returns(new[]
|
||||
{
|
||||
new TorrentContent { Index = 0, Priority = TorrentContentPriority.Normal }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true, true));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -943,7 +1056,7 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
// Slow check is skipped because not in downloading state
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
.Returns((true, DeleteReason.Stalled, true, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -1001,11 +1114,11 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<QBitItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -132,11 +132,11 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -182,11 +182,11 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -300,11 +300,11 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -399,7 +399,7 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -447,7 +447,7 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -495,7 +495,7 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
.Returns((true, DeleteReason.SlowSpeed, true, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -504,6 +504,55 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
|
||||
{
|
||||
// Arrange
|
||||
const string hash = "TEST-HASH";
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var download = new RTorrentTorrent
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Test Torrent",
|
||||
IsPrivate = 0,
|
||||
State = 1,
|
||||
Complete = 0,
|
||||
DownRate = 1000,
|
||||
SizeBytes = 1000,
|
||||
CompletedBytes = 500
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(download);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTrackersAsync(hash)
|
||||
.Returns(new List<string>());
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, false, true));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
// Assert
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,7 +599,7 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -598,7 +647,7 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
.Returns((true, DeleteReason.Stalled, true, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -607,6 +656,7 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,7 +704,7 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
// Slow check is skipped because speed is 0
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
.Returns((true, DeleteReason.Stalled, true, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
@@ -704,11 +754,11 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<RTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
// Act
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -108,11 +108,11 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -167,11 +167,11 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -294,11 +294,11 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -476,11 +476,11 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -542,7 +542,7 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -598,7 +598,7 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -661,13 +661,14 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
.Returns((true, DeleteReason.SlowSpeed, true, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -719,13 +720,52 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
.Returns((true, DeleteReason.Stalled, true, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
|
||||
{
|
||||
const string hash = "test-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var torrentInfo = new TorrentInfo
|
||||
{
|
||||
Id = 1,
|
||||
HashString = hash,
|
||||
Name = "Test Torrent",
|
||||
Status = 4,
|
||||
IsPrivate = false,
|
||||
RateDownload = 1000,
|
||||
FileStats = new[] { new TransmissionTorrentFileStats { Wanted = true } }
|
||||
};
|
||||
|
||||
var torrents = new TransmissionTorrents
|
||||
{
|
||||
Torrents = new[] { torrentInfo }
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.TorrentGetAsync(Arg.Any<string[]>(), hash)
|
||||
.Returns(torrents);
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<TransmissionItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, false, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,11 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -129,11 +129,11 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -231,11 +231,11 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -399,11 +399,11 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -456,7 +456,7 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -502,7 +502,7 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((false, DeleteReason.None, false));
|
||||
.Returns((false, DeleteReason.None, false, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
@@ -555,13 +555,14 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, true));
|
||||
.Returns((true, DeleteReason.SlowSpeed, true, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -603,13 +604,111 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true));
|
||||
.Returns((true, DeleteReason.Stalled, true, false));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SlowDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
|
||||
{
|
||||
const string hash = "test-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var torrentItem = new UTorrentItem
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Test Torrent",
|
||||
Status = 9,
|
||||
DownloadSpeed = 1000
|
||||
};
|
||||
|
||||
var torrentProperties = new UTorrentProperties
|
||||
{
|
||||
Hash = hash,
|
||||
Pex = 1,
|
||||
Trackers = ""
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateSlowRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.SlowSpeed, false, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StalledDownload_RuleWithChangeCategory_PropagatesChangeCategoryFlag()
|
||||
{
|
||||
const string hash = "test-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var torrentItem = new UTorrentItem
|
||||
{
|
||||
Hash = hash,
|
||||
Name = "Test Torrent",
|
||||
Status = 9,
|
||||
DownloadSpeed = 0,
|
||||
ETA = 0
|
||||
};
|
||||
|
||||
var torrentProperties = new UTorrentProperties
|
||||
{
|
||||
Hash = hash,
|
||||
Pex = 1,
|
||||
Trackers = ""
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentAsync(hash)
|
||||
.Returns(torrentItem);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentPropertiesAsync(hash)
|
||||
.Returns(torrentProperties);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFilesAsync(hash)
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.RuleEvaluator
|
||||
.EvaluateStallRulesAsync(Arg.Any<UTorrentItemWrapper>())
|
||||
.Returns((true, DeleteReason.Stalled, true, true));
|
||||
|
||||
var result = await sut.ShouldRemoveFromArrQueueAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.Stalled);
|
||||
result.DeleteFromClient.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
request.Instance,
|
||||
request.Record,
|
||||
request.RemoveFromClient,
|
||||
request.ChangeCategory,
|
||||
request.DeleteReason);
|
||||
}
|
||||
|
||||
@@ -288,6 +289,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<DeleteReason>())
|
||||
.ThrowsAsync(new HttpRequestException("Not found", null, HttpStatusCode.NotFound));
|
||||
|
||||
@@ -312,6 +314,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<DeleteReason>())
|
||||
.ThrowsAsync(new HttpRequestException("Not found", null, HttpStatusCode.NotFound));
|
||||
|
||||
@@ -335,6 +338,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<DeleteReason>())
|
||||
.ThrowsAsync(originalException);
|
||||
|
||||
@@ -357,6 +361,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<DeleteReason>())
|
||||
.ThrowsAsync(originalException);
|
||||
|
||||
@@ -390,6 +395,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<bool>(),
|
||||
deleteReason);
|
||||
}
|
||||
|
||||
@@ -408,7 +414,28 @@ public class QueueItemRemoverTests : IDisposable
|
||||
await _arrClient.Received(1).DeleteQueueItemAsync(
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
removeFromClient,
|
||||
Arg.Is<bool>(x => x == removeFromClient),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<DeleteReason>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task RemoveQueueItemAsync_PassesCorrectChangeCategoryFlag(bool changeCategory)
|
||||
{
|
||||
// Arrange
|
||||
var request = CreateRemoveRequest(changeCategory: changeCategory);
|
||||
|
||||
// Act
|
||||
await _queueItemRemover.RemoveQueueItemAsync(request);
|
||||
|
||||
// Assert
|
||||
await _arrClient.Received(1).DeleteQueueItemAsync(
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Is<bool>(x => x == changeCategory),
|
||||
Arg.Any<DeleteReason>());
|
||||
}
|
||||
|
||||
@@ -420,7 +447,8 @@ public class QueueItemRemoverTests : IDisposable
|
||||
InstanceType instanceType = InstanceType.Sonarr,
|
||||
bool removeFromClient = true,
|
||||
DeleteReason deleteReason = DeleteReason.Stalled,
|
||||
bool skipSearch = false)
|
||||
bool skipSearch = false,
|
||||
bool changeCategory = false)
|
||||
{
|
||||
// Use an ArrInstance that exists in the DB to satisfy FK constraint on SearchQueueItem
|
||||
var instance = GetOrCreateArrInstance(instanceType);
|
||||
@@ -431,6 +459,7 @@ public class QueueItemRemoverTests : IDisposable
|
||||
SearchItem = new SearchItem { Id = 123 },
|
||||
Record = CreateQueueRecord(),
|
||||
RemoveFromClient = removeFromClient,
|
||||
ChangeCategory = changeCategory,
|
||||
DeleteReason = deleteReason,
|
||||
SkipSearch = skipSearch,
|
||||
JobRunId = _jobRunId
|
||||
|
||||
@@ -88,7 +88,7 @@ public class MalwareBlockerIntegrationTests : IDisposable
|
||||
|
||||
// Process through real QueueItemRemover
|
||||
_fixture.ArrClient.DeleteQueueItemAsync(
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _fixture.ProcessCapturedRemoveRequestsAsync();
|
||||
@@ -98,6 +98,7 @@ public class MalwareBlockerIntegrationTests : IDisposable
|
||||
Arg.Is<ArrInstance>(i => i.Id == instance.Id),
|
||||
Arg.Is<QueueRecord>(r => r.DownloadId == record.DownloadId),
|
||||
true,
|
||||
false,
|
||||
DeleteReason.AllFilesBlocked);
|
||||
|
||||
// Assert: Full event property verification
|
||||
@@ -215,7 +216,7 @@ public class MalwareBlockerIntegrationTests : IDisposable
|
||||
_fixture.GetCapturedRemoveRequests().Count.ShouldBe(1);
|
||||
|
||||
_fixture.ArrClient.DeleteQueueItemAsync(
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _fixture.ProcessCapturedRemoveRequestsAsync();
|
||||
@@ -224,6 +225,7 @@ public class MalwareBlockerIntegrationTests : IDisposable
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
false,
|
||||
false,
|
||||
DeleteReason.AllFilesBlocked);
|
||||
|
||||
// Full event property verification
|
||||
|
||||
@@ -81,7 +81,7 @@ public class QueueCleanerIntegrationTests : IDisposable
|
||||
|
||||
// Process the captured messages through the real QueueItemRemover pipeline
|
||||
_fixture.ArrClient.DeleteQueueItemAsync(
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _fixture.ProcessCapturedRemoveRequestsAsync();
|
||||
@@ -91,6 +91,7 @@ public class QueueCleanerIntegrationTests : IDisposable
|
||||
Arg.Is<ArrInstance>(i => i.Id == instance.Id),
|
||||
Arg.Is<QueueRecord>(r => r.DownloadId == record.DownloadId),
|
||||
true,
|
||||
false,
|
||||
DeleteReason.Stalled);
|
||||
|
||||
// Assert Phase 3: Events persisted with full property verification
|
||||
@@ -185,7 +186,7 @@ public class QueueCleanerIntegrationTests : IDisposable
|
||||
_fixture.GetCapturedRemoveRequests().Count.ShouldBe(1);
|
||||
|
||||
_fixture.ArrClient.DeleteQueueItemAsync(
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _fixture.ProcessCapturedRemoveRequestsAsync();
|
||||
@@ -280,7 +281,7 @@ public class QueueCleanerIntegrationTests : IDisposable
|
||||
_fixture.GetCapturedRemoveRequests().Count.ShouldBe(1);
|
||||
|
||||
_fixture.ArrClient.DeleteQueueItemAsync(
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
Arg.Any<ArrInstance>(), Arg.Any<QueueRecord>(), Arg.Any<bool>(), Arg.Any<bool>(), Arg.Any<DeleteReason>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await _fixture.ProcessCapturedRemoveRequestsAsync();
|
||||
@@ -290,6 +291,7 @@ public class QueueCleanerIntegrationTests : IDisposable
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
false,
|
||||
false,
|
||||
DeleteReason.Stalled);
|
||||
|
||||
// Full event property verification
|
||||
|
||||
@@ -1334,4 +1334,163 @@ public class QueueCleanerTests : IDisposable
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ChangeCategory Tests
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessInstanceAsync_WhenFailedImportWithChangeCategory_PublishesRequestWithChangeCategoryAndRemoveFromClientFalse()
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var queueCleanerConfig = _fixture.DataContext.QueueCleanerConfigs.First();
|
||||
// Set DeletePrivate = true so RemoveFromClient would be true without the ChangeCategory override.
|
||||
// This makes the RemoveFromClient == false assertion below conclusive.
|
||||
queueCleanerConfig.FailedImport = queueCleanerConfig.FailedImport with { ChangeCategory = true, DeletePrivate = false };
|
||||
// Validate gate prevents both flags being true at once; we keep DeletePrivate=false here, but rely on
|
||||
// IsPrivate=false from the mock so removeFromClient resolves to !changeCategory.
|
||||
_fixture.DataContext.SaveChanges();
|
||||
|
||||
var mockArrClient = Substitute.For<IArrClient>();
|
||||
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
||||
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
||||
mockArrClient.ShouldRemoveFromQueue(
|
||||
Arg.Any<InstanceType>(),
|
||||
Arg.Any<QueueRecord>(),
|
||||
Arg.Any<bool>(),
|
||||
Arg.Any<short>()
|
||||
).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "failed-import-change-category",
|
||||
Title = "Failed Import Change Category",
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Iterate(
|
||||
Arg.Any<IArrClient>(),
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
)
|
||||
.Returns(async ci =>
|
||||
{
|
||||
var callback = ci.ArgAt<Func<IReadOnlyList<QueueRecord>, Task>>(2);
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.ShouldRemoveFromArrQueueAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<List<string>>()
|
||||
)
|
||||
// IsPrivate=false ensures the failed-import path computes
|
||||
// removeFromClient = !changeCategory && (!IsPrivate || DeletePrivate) = !changeCategory && true.
|
||||
// So RemoveFromClient == false in the assertion is only satisfiable due to changeCategory=true.
|
||||
.Returns(new DownloadCheckResult { Found = true, ShouldRemove = false, IsPrivate = false });
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(mockDownloadService);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await _fixture.MessageBus.Received(1).Publish(
|
||||
Arg.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
|
||||
r.DeleteReason == DeleteReason.FailedImport &&
|
||||
r.ChangeCategory == true &&
|
||||
r.RemoveFromClient == false
|
||||
),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessInstanceAsync_WhenStallRuleHasChangeCategory_PublishesRequestWithChangeCategoryAndRemoveFromClientFalse()
|
||||
{
|
||||
// Arrange
|
||||
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
|
||||
|
||||
var mockArrClient = Substitute.For<IArrClient>();
|
||||
mockArrClient.IsRecordValid(Arg.Any<QueueRecord>()).Returns(true);
|
||||
mockArrClient.HasContentId(Arg.Any<QueueRecord>()).Returns(true);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
var queueRecord = new QueueRecord
|
||||
{
|
||||
Id = 1,
|
||||
DownloadId = "stall-change-category",
|
||||
Title = "Stall Change Category",
|
||||
Protocol = "torrent",
|
||||
SeriesId = 1,
|
||||
EpisodeId = 1
|
||||
};
|
||||
|
||||
_fixture.ArrQueueIterator
|
||||
.Iterate(
|
||||
Arg.Any<IArrClient>(),
|
||||
Arg.Any<ArrInstance>(),
|
||||
Arg.Any<Func<IReadOnlyList<QueueRecord>, Task>>()
|
||||
)
|
||||
.Returns(async ci =>
|
||||
{
|
||||
var callback = ci.ArgAt<Func<IReadOnlyList<QueueRecord>, Task>>(2);
|
||||
await callback([queueRecord]);
|
||||
});
|
||||
|
||||
var mockDownloadService = _fixture.CreateMockDownloadService();
|
||||
mockDownloadService
|
||||
.ShouldRemoveFromArrQueueAsync(
|
||||
Arg.Any<string>(),
|
||||
Arg.Any<List<string>>()
|
||||
)
|
||||
.Returns(new DownloadCheckResult
|
||||
{
|
||||
Found = true,
|
||||
ShouldRemove = true,
|
||||
IsPrivate = true,
|
||||
DeleteFromClient = true,
|
||||
ChangeCategory = true,
|
||||
DeleteReason = DeleteReason.Stalled,
|
||||
});
|
||||
|
||||
_fixture.DownloadServiceFactory
|
||||
.GetDownloadService(Arg.Any<DownloadClientConfig>())
|
||||
.Returns(mockDownloadService);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await _fixture.MessageBus.Received(1).Publish(
|
||||
Arg.Is<QueueItemRemoveRequest<SeriesSearchItem>>(r =>
|
||||
r.DeleteReason == DeleteReason.Stalled &&
|
||||
r.ChangeCategory == true &&
|
||||
r.RemoveFromClient == false
|
||||
),
|
||||
Arg.Any<CancellationToken>()
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -694,7 +694,13 @@ public class QueueRuleEvaluatorTests : IDisposable
|
||||
return torrent;
|
||||
}
|
||||
|
||||
private static StallRule CreateStallRule(string name, bool resetOnProgress, int maxStrikes, string? minimumProgress = null, bool deletePrivateTorrentsFromClient = false)
|
||||
private static StallRule CreateStallRule(
|
||||
string name,
|
||||
bool resetOnProgress,
|
||||
int maxStrikes,
|
||||
string? minimumProgress = null,
|
||||
bool deletePrivateTorrentsFromClient = false,
|
||||
bool changeCategory = false)
|
||||
{
|
||||
return new StallRule
|
||||
{
|
||||
@@ -709,6 +715,7 @@ public class QueueRuleEvaluatorTests : IDisposable
|
||||
ResetStrikesOnProgress = resetOnProgress,
|
||||
MinimumProgress = minimumProgress,
|
||||
DeletePrivateTorrentsFromClient = deletePrivateTorrentsFromClient,
|
||||
ChangeCategory = changeCategory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -718,7 +725,8 @@ public class QueueRuleEvaluatorTests : IDisposable
|
||||
int maxStrikes,
|
||||
string? minSpeed = null,
|
||||
double maxTimeHours = 1,
|
||||
bool deletePrivateTorrentsFromClient = false)
|
||||
bool deletePrivateTorrentsFromClient = false,
|
||||
bool changeCategory = false)
|
||||
{
|
||||
return new SlowRule
|
||||
{
|
||||
@@ -735,6 +743,7 @@ public class QueueRuleEvaluatorTests : IDisposable
|
||||
MinSpeed = minSpeed ?? string.Empty,
|
||||
IgnoreAboveSize = string.Empty,
|
||||
DeletePrivateTorrentsFromClient = deletePrivateTorrentsFromClient,
|
||||
ChangeCategory = changeCategory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -993,4 +1002,140 @@ public class QueueRuleEvaluatorTests : IDisposable
|
||||
result.Reason.ShouldBe(DeleteReason.None);
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
}
|
||||
|
||||
#region ChangeCategory Tests
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesWithChangeCategory_ShouldReturnChangeCategoryTrueAndDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Change Category", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: true, changeCategory: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.Reason.ShouldBe(DeleteReason.Stalled);
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateStallRulesAsync_WhenRuleMatchesWithoutChangeCategory_ShouldReturnChangeCategoryFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var stallRule = CreateStallRule("Stall Default", resetOnProgress: false, maxStrikes: 3, deletePrivateTorrentsFromClient: false, changeCategory: false);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingStallRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(stallRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.Stalled, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateStallRulesAsync(torrent);
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.ChangeCategory.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenSpeedRuleMatchesWithChangeCategory_ShouldReturnChangeCategoryTrueAndDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
"Slow Speed Change Category",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
minSpeed: "5 MB",
|
||||
maxTimeHours: 0,
|
||||
deletePrivateTorrentsFromClient: true,
|
||||
changeCategory: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowSpeed, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
torrent.DownloadSpeed.Returns(ByteSize.Parse("1 MB").Bytes);
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.Reason.ShouldBe(DeleteReason.SlowSpeed);
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateSlowRulesAsync_WhenTimeRuleMatchesWithChangeCategory_ShouldReturnChangeCategoryTrueAndDeleteFromClientFalse()
|
||||
{
|
||||
var ruleManager = Substitute.For<IQueueRuleManager>();
|
||||
var striker = Substitute.For<IStriker>();
|
||||
var logger = Substitute.For<ILogger<QueueRuleEvaluator>>();
|
||||
var context = CreateInMemoryEventsContext();
|
||||
|
||||
var evaluator = new QueueRuleEvaluator(ruleManager, striker, context, logger);
|
||||
|
||||
var slowRule = CreateSlowRule(
|
||||
"Slow Time Change Category",
|
||||
resetOnProgress: false,
|
||||
maxStrikes: 3,
|
||||
maxTimeHours: 1,
|
||||
deletePrivateTorrentsFromClient: true,
|
||||
changeCategory: true);
|
||||
|
||||
ruleManager
|
||||
.GetMatchingSlowRule(Arg.Any<ITorrentItemWrapper>())
|
||||
.Returns(slowRule);
|
||||
|
||||
striker
|
||||
.StrikeAndCheckLimit(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<ushort>(), StrikeType.SlowTime, Arg.Any<long?>())
|
||||
.Returns(true);
|
||||
|
||||
var torrent = CreateTorrentMock();
|
||||
|
||||
var result = await evaluator.EvaluateSlowRulesAsync(torrent);
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.Reason.ShouldBe(DeleteReason.SlowTime);
|
||||
result.ChangeCategory.ShouldBeTrue();
|
||||
result.DeleteFromClient.ShouldBeFalse();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -163,12 +163,13 @@ public abstract class ArrClient : IArrClient
|
||||
ArrInstance arrInstance,
|
||||
QueueRecord record,
|
||||
bool removeFromClient,
|
||||
bool changeCategory,
|
||||
DeleteReason deleteReason
|
||||
)
|
||||
{
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/{GetQueueDeleteUrlPath(record.Id).TrimStart('/')}";
|
||||
uriBuilder.Query = GetQueueDeleteUrlQuery(removeFromClient);
|
||||
uriBuilder.Query = GetQueueDeleteUrlQuery(removeFromClient, changeCategory);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -177,11 +178,23 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
HttpResponseMessage? response = await _dryRunInterceptor.InterceptAsync<HttpResponseMessage>(SendRequestAsync, request);
|
||||
response?.Dispose();
|
||||
|
||||
|
||||
string logMessage;
|
||||
if (changeCategory)
|
||||
{
|
||||
logMessage = "queue item category changed in arr with reason {reason} | {url} | {title}";
|
||||
}
|
||||
else if (removeFromClient)
|
||||
{
|
||||
logMessage = "queue item deleted with reason {reason} | {url} | {title}";
|
||||
}
|
||||
else
|
||||
{
|
||||
logMessage = "queue item removed from arr with reason {reason} | {url} | {title}";
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
removeFromClient
|
||||
? "queue item deleted with reason {reason} | {url} | {title}"
|
||||
: "queue item removed from arr with reason {reason} | {url} | {title}",
|
||||
logMessage,
|
||||
deleteReason.ToString(),
|
||||
arrInstance.Url,
|
||||
record.Title
|
||||
@@ -262,7 +275,21 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
protected abstract string GetQueueDeleteUrlPath(long recordId);
|
||||
|
||||
protected abstract string GetQueueDeleteUrlQuery(bool removeFromClient);
|
||||
protected virtual string GetQueueDeleteUrlQuery(bool removeFromClient, bool changeCategory)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&";
|
||||
|
||||
if (changeCategory)
|
||||
{
|
||||
query += "changeCategory=true&removeFromClient=false";
|
||||
return query;
|
||||
}
|
||||
|
||||
query += "changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
protected virtual void SetApiKey(HttpRequestMessage request, string apiKey)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,15 @@ public interface IArrClient
|
||||
|
||||
Task<bool> ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload, short arrMaxStrikes);
|
||||
|
||||
Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, DeleteReason deleteReason);
|
||||
/// <summary>
|
||||
/// Removes a queue item from the *arr instance.
|
||||
/// </summary>
|
||||
/// <param name="arrInstance">The *arr instance hosting the queue item.</param>
|
||||
/// <param name="record">The queue record to remove.</param>
|
||||
/// <param name="removeFromClient">When true, also delete the download from the download client. Ignored when <paramref name="changeCategory"/> is true.</param>
|
||||
/// <param name="changeCategory">When true, instructs the *arr to change the download's category to the post-import category instead of removing it from the download client. Mutually exclusive with <paramref name="removeFromClient"/>.</param>
|
||||
/// <param name="deleteReason">Reason for removal, used for logging and event publishing.</param>
|
||||
Task DeleteQueueItemAsync(ArrInstance arrInstance, QueueRecord record, bool removeFromClient, bool changeCategory, DeleteReason deleteReason);
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a search for the specified items and returns the arr command IDs
|
||||
|
||||
@@ -42,14 +42,6 @@ public class LidarrClient : ArrClient, ILidarrClient
|
||||
return $"/api/v1/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task<List<long>> SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
|
||||
@@ -42,14 +42,6 @@ public class RadarrClient : ArrClient, IRadarrClient
|
||||
return $"/api/v3/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task<List<long>> SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
|
||||
@@ -42,14 +42,6 @@ public class ReadarrClient : ArrClient, IReadarrClient
|
||||
return $"/api/v1/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task<List<long>> SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
|
||||
@@ -44,14 +44,6 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
return $"/api/v3/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task<List<long>> SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
|
||||
@@ -44,14 +44,6 @@ public class WhisparrV2Client : ArrClient, IWhisparrV2Client
|
||||
return $"/api/v3/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task<List<long>> SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
|
||||
@@ -43,14 +43,6 @@ public class WhisparrV3Client : ArrClient, IWhisparrV3Client
|
||||
return $"/api/v3/queue/{recordId}";
|
||||
}
|
||||
|
||||
protected override string GetQueueDeleteUrlQuery(bool removeFromClient)
|
||||
{
|
||||
string query = "blocklist=true&skipRedownload=true&changeCategory=false";
|
||||
query += removeFromClient ? "&removeFromClient=true" : "&removeFromClient=false";
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public override async Task<List<long>> SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items)
|
||||
{
|
||||
if (items?.Count is null or 0)
|
||||
|
||||
@@ -71,14 +71,14 @@ public partial class DelugeService
|
||||
}
|
||||
|
||||
// remove if download is stuck
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient) = await EvaluateDownloadRemoval(torrent);
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient, result.ChangeCategory) = await EvaluateDownloadRemoval(torrent);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient) result = await CheckIfSlow(wrapper);
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
@@ -88,29 +88,29 @@ public partial class DelugeService
|
||||
return await CheckIfStuck(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsDownloading())
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download is not in downloading state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsStalled())
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is not in stalled state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
|
||||
@@ -22,4 +22,9 @@ public sealed record DownloadCheckResult
|
||||
/// True if the download should be deleted from the client; otherwise false.
|
||||
/// </summary>
|
||||
public bool DeleteFromClient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the matching queue rule asked to change the category in the *arr instead of deleting; otherwise false.
|
||||
/// </summary>
|
||||
public bool ChangeCategory { get; set; }
|
||||
}
|
||||
@@ -71,14 +71,14 @@ public partial class QBitService
|
||||
return result;
|
||||
}
|
||||
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient) = await EvaluateDownloadRemoval(torrent);
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient, result.ChangeCategory) = await EvaluateDownloadRemoval(torrent);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient) slowResult = await CheckIfSlow(wrapper);
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) slowResult = await CheckIfSlow(wrapper);
|
||||
|
||||
if (slowResult.ShouldRemove)
|
||||
{
|
||||
@@ -88,24 +88,24 @@ public partial class QBitService
|
||||
return await CheckIfStuck(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsDownloading())
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download is not in downloading state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (((QBitItemWrapper)wrapper).IsMetadataDownloading())
|
||||
{
|
||||
@@ -119,17 +119,17 @@ public partial class QBitService
|
||||
queueCleanerConfig.DownloadingMetadataMaxStrikes,
|
||||
StrikeType.DownloadingMetadata
|
||||
);
|
||||
|
||||
return (shouldRemove, DeleteReason.DownloadingMetadata, shouldRemove);
|
||||
|
||||
return (shouldRemove, DeleteReason.DownloadingMetadata, shouldRemove, false);
|
||||
}
|
||||
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
if (!wrapper.IsStalled())
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is not in stalled state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
|
||||
@@ -62,14 +62,14 @@ public partial class RTorrentService
|
||||
}
|
||||
|
||||
// remove if download is stuck
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient) = await EvaluateDownloadRemoval(torrent);
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient, result.ChangeCategory) = await EvaluateDownloadRemoval(torrent);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient) result = await CheckIfSlow(wrapper);
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
@@ -79,29 +79,29 @@ public partial class RTorrentService
|
||||
return await CheckIfStuck(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsDownloading())
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download is not in downloading state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsStalled())
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is not in stalled state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
|
||||
@@ -66,7 +66,7 @@ public partial class TransmissionService
|
||||
}
|
||||
|
||||
// remove if download is stuck
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient) = await EvaluateDownloadRemoval(torrent);
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient, result.ChangeCategory) = await EvaluateDownloadRemoval(torrent);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -80,9 +80,9 @@ public partial class TransmissionService
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient) result = await CheckIfSlow(wrapper);
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
@@ -93,29 +93,29 @@ public partial class TransmissionService
|
||||
}
|
||||
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsDownloading())
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download is not in downloading state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsStalled())
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is not in stalled state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
|
||||
@@ -70,14 +70,14 @@ public partial class UTorrentService
|
||||
}
|
||||
|
||||
// remove if download is stuck
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient) = await EvaluateDownloadRemoval(torrent);
|
||||
(result.ShouldRemove, result.DeleteReason, result.DeleteFromClient, result.ChangeCategory) = await EvaluateDownloadRemoval(torrent);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient) result = await CheckIfSlow(wrapper);
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
@@ -88,29 +88,29 @@ public partial class UTorrentService
|
||||
}
|
||||
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsDownloading())
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download is not in downloading state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsStalled())
|
||||
{
|
||||
_logger.LogTrace("skip stalled check | download is not in stalled state | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
|
||||
@@ -17,6 +17,12 @@ public sealed record QueueItemRemoveRequest<T>
|
||||
|
||||
public required bool RemoveFromClient { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When true, the *arr is asked to change the download's category to its post-import category
|
||||
/// instead of removing it from the download client. Mutually exclusive with <see cref="RemoveFromClient"/>.
|
||||
/// </summary>
|
||||
public bool ChangeCategory { get; init; }
|
||||
|
||||
public required DeleteReason DeleteReason { get; init; }
|
||||
|
||||
public required Guid JobRunId { get; init; }
|
||||
|
||||
@@ -51,7 +51,7 @@ public sealed class QueueItemRemover : IQueueItemRemover
|
||||
{
|
||||
var instanceType = request.Instance.ArrConfig.Type;
|
||||
var arrClient = _arrClientFactory.GetClient(instanceType, request.Instance.Version);
|
||||
await arrClient.DeleteQueueItemAsync(request.Instance, request.Record, request.RemoveFromClient, request.DeleteReason);
|
||||
await arrClient.DeleteQueueItemAsync(request.Instance, request.Record, request.RemoveFromClient, request.ChangeCategory, request.DeleteReason);
|
||||
|
||||
// Mark the download item as removed in the database
|
||||
await _eventsContext.DownloadItems
|
||||
|
||||
@@ -131,7 +131,8 @@ public abstract class GenericHandler : IHandler
|
||||
bool removeFromClient,
|
||||
DeleteReason deleteReason,
|
||||
bool skipSearch = false,
|
||||
DownloadClientConfig? downloadClient = null
|
||||
DownloadClientConfig? downloadClient = null,
|
||||
bool changeCategory = false
|
||||
)
|
||||
{
|
||||
if (_cache.TryGetValue(downloadRemovalKey, out bool _))
|
||||
@@ -150,6 +151,7 @@ public abstract class GenericHandler : IHandler
|
||||
Record = record,
|
||||
SearchItem = (SeriesSearchItem)GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
ChangeCategory = changeCategory,
|
||||
DeleteReason = deleteReason,
|
||||
JobRunId = ContextProvider.GetJobRunId(),
|
||||
SkipSearch = skipSearch,
|
||||
@@ -166,6 +168,7 @@ public abstract class GenericHandler : IHandler
|
||||
Record = record,
|
||||
SearchItem = GetRecordSearchItem(instanceType, instance.Version, record, isPack),
|
||||
RemoveFromClient = removeFromClient,
|
||||
ChangeCategory = changeCategory,
|
||||
DeleteReason = deleteReason,
|
||||
JobRunId = ContextProvider.GetJobRunId(),
|
||||
SkipSearch = skipSearch,
|
||||
|
||||
@@ -192,7 +192,8 @@ public sealed class QueueCleaner : GenericHandler
|
||||
|
||||
if (downloadCheckResult.ShouldRemove)
|
||||
{
|
||||
bool removeFromClient = !downloadCheckResult.IsPrivate || downloadCheckResult.DeleteFromClient;
|
||||
bool changeCategory = downloadCheckResult.ChangeCategory;
|
||||
bool removeFromClient = !changeCategory && (!downloadCheckResult.IsPrivate || downloadCheckResult.DeleteFromClient);
|
||||
|
||||
await PublishQueueItemRemoveRequest(
|
||||
downloadRemovalKey,
|
||||
@@ -202,7 +203,8 @@ public sealed class QueueCleaner : GenericHandler
|
||||
removeFromClient,
|
||||
downloadCheckResult.DeleteReason,
|
||||
skipSearch: !hasContentId,
|
||||
downloadClient: foundInClient
|
||||
downloadClient: foundInClient,
|
||||
changeCategory: changeCategory
|
||||
);
|
||||
|
||||
continue;
|
||||
@@ -221,7 +223,8 @@ public sealed class QueueCleaner : GenericHandler
|
||||
|
||||
if (shouldRemoveFromArr)
|
||||
{
|
||||
bool removeFromClient = !downloadCheckResult.IsPrivate || queueCleanerConfig.FailedImport.DeletePrivate;
|
||||
bool changeCategory = queueCleanerConfig.FailedImport.ChangeCategory;
|
||||
bool removeFromClient = !changeCategory && (!downloadCheckResult.IsPrivate || queueCleanerConfig.FailedImport.DeletePrivate);
|
||||
|
||||
await PublishQueueItemRemoveRequest(
|
||||
downloadRemovalKey,
|
||||
@@ -231,7 +234,8 @@ public sealed class QueueCleaner : GenericHandler
|
||||
removeFromClient,
|
||||
DeleteReason.FailedImport,
|
||||
skipSearch: !hasContentId,
|
||||
downloadClient: foundInClient
|
||||
downloadClient: foundInClient,
|
||||
changeCategory: changeCategory
|
||||
);
|
||||
|
||||
continue;
|
||||
|
||||
@@ -6,6 +6,6 @@ namespace Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
|
||||
public interface IQueueRuleEvaluator
|
||||
{
|
||||
Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateStallRulesAsync(ITorrentItemWrapper torrent);
|
||||
Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateSlowRulesAsync(ITorrentItemWrapper torrent);
|
||||
Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> EvaluateStallRulesAsync(ITorrentItemWrapper torrent);
|
||||
Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> EvaluateSlowRulesAsync(ITorrentItemWrapper torrent);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateStallRulesAsync(ITorrentItemWrapper torrent)
|
||||
public async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> EvaluateStallRulesAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
_logger.LogTrace("Evaluating stall rules | {name}", torrent.Name);
|
||||
|
||||
@@ -38,7 +38,7 @@ public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
if (rule is null)
|
||||
{
|
||||
_logger.LogTrace("skip | no stall rules matched | {name}", torrent.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
_logger.LogTrace("Applying stall rule {rule} | {name}", rule.Name, torrent.Name);
|
||||
@@ -61,13 +61,14 @@ public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
|
||||
if (shouldRemove)
|
||||
{
|
||||
return (true, DeleteReason.Stalled, rule.DeletePrivateTorrentsFromClient);
|
||||
bool deleteFromClient = rule is { ChangeCategory: false, DeletePrivateTorrentsFromClient: true };
|
||||
return (true, DeleteReason.Stalled, deleteFromClient, rule.ChangeCategory);
|
||||
}
|
||||
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
public async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient)> EvaluateSlowRulesAsync(ITorrentItemWrapper torrent)
|
||||
public async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> EvaluateSlowRulesAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
_logger.LogTrace("Evaluating slow rules | {name}", torrent.Name);
|
||||
|
||||
@@ -76,7 +77,7 @@ public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
if (rule is null)
|
||||
{
|
||||
_logger.LogDebug("skip | no slow rules matched | {name}", torrent.Name);
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
_logger.LogTrace("Applying slow rule {rule} | {name}", rule.Name, torrent.Name);
|
||||
@@ -97,7 +98,8 @@ public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
|
||||
if (shouldRemove)
|
||||
{
|
||||
return (true, DeleteReason.SlowSpeed, rule.DeletePrivateTorrentsFromClient);
|
||||
bool deleteFromClient = rule is { ChangeCategory: false, DeletePrivateTorrentsFromClient: true };
|
||||
return (true, DeleteReason.SlowSpeed, deleteFromClient, rule.ChangeCategory);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -121,7 +123,8 @@ public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
|
||||
if (shouldRemove)
|
||||
{
|
||||
return (true, DeleteReason.SlowTime, rule.DeletePrivateTorrentsFromClient);
|
||||
bool deleteFromClient = rule is { ChangeCategory: false, DeletePrivateTorrentsFromClient: true };
|
||||
return (true, DeleteReason.SlowTime, deleteFromClient, rule.ChangeCategory);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -130,7 +133,7 @@ public class QueueRuleEvaluator : IQueueRuleEvaluator
|
||||
}
|
||||
}
|
||||
|
||||
return (false, DeleteReason.None, false);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
private async Task ResetStalledStrikesAsync(
|
||||
|
||||
@@ -167,4 +167,77 @@ public sealed class FailedImportConfigTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate - ChangeCategory Validation
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryDefault_DoesNotThrow()
|
||||
{
|
||||
var config = new FailedImportConfig
|
||||
{
|
||||
MaxStrikes = 3,
|
||||
PatternMode = PatternMode.Exclude,
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryAndDeletePrivateBothFalse_DoesNotThrow()
|
||||
{
|
||||
var config = new FailedImportConfig
|
||||
{
|
||||
MaxStrikes = 3,
|
||||
PatternMode = PatternMode.Exclude,
|
||||
ChangeCategory = false,
|
||||
DeletePrivate = false,
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryTrueAndDeletePrivateFalse_DoesNotThrow()
|
||||
{
|
||||
var config = new FailedImportConfig
|
||||
{
|
||||
MaxStrikes = 3,
|
||||
PatternMode = PatternMode.Exclude,
|
||||
ChangeCategory = true,
|
||||
DeletePrivate = false,
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryFalseAndDeletePrivateTrue_DoesNotThrow()
|
||||
{
|
||||
var config = new FailedImportConfig
|
||||
{
|
||||
MaxStrikes = 3,
|
||||
PatternMode = PatternMode.Exclude,
|
||||
ChangeCategory = false,
|
||||
DeletePrivate = true,
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryAndDeletePrivateBothTrue_ThrowsValidationException()
|
||||
{
|
||||
var config = new FailedImportConfig
|
||||
{
|
||||
MaxStrikes = 3,
|
||||
PatternMode = PatternMode.Exclude,
|
||||
ChangeCategory = true,
|
||||
DeletePrivate = true,
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
exception.Message.ShouldBe("Cannot enable both deletion and category changing");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -291,4 +291,63 @@ public sealed class QueueRuleTests
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Validate - ChangeCategory Validation
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryDefault_DoesNotThrow()
|
||||
{
|
||||
var rule = new StallRule
|
||||
{
|
||||
Name = "test-rule",
|
||||
MaxStrikes = 3,
|
||||
};
|
||||
|
||||
Should.NotThrow(() => rule.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryTrueAndDeletePrivateFromClientFalse_DoesNotThrow()
|
||||
{
|
||||
var rule = new StallRule
|
||||
{
|
||||
Name = "test-rule",
|
||||
MaxStrikes = 3,
|
||||
ChangeCategory = true,
|
||||
DeletePrivateTorrentsFromClient = false,
|
||||
};
|
||||
|
||||
Should.NotThrow(() => rule.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryFalseAndDeletePrivateFromClientTrue_DoesNotThrow()
|
||||
{
|
||||
var rule = new StallRule
|
||||
{
|
||||
Name = "test-rule",
|
||||
MaxStrikes = 3,
|
||||
ChangeCategory = false,
|
||||
DeletePrivateTorrentsFromClient = true,
|
||||
};
|
||||
|
||||
Should.NotThrow(() => rule.Validate());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_WithChangeCategoryAndDeletePrivateFromClientBothTrue_ThrowsValidationException()
|
||||
{
|
||||
var rule = new StallRule
|
||||
{
|
||||
Name = "test-rule",
|
||||
MaxStrikes = 3,
|
||||
ChangeCategory = true,
|
||||
DeletePrivateTorrentsFromClient = true,
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => rule.Validate());
|
||||
exception.Message.ShouldBe("Cannot enable both deletion and category changing");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
2120
code/backend/Cleanuparr.Persistence/Migrations/Data/20260504193343_AddQueueCleanerChangeCategory.Designer.cs
generated
Normal file
2120
code/backend/Cleanuparr.Persistence/Migrations/Data/20260504193343_AddQueueCleanerChangeCategory.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddQueueCleanerChangeCategory : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "change_category",
|
||||
table: "stall_rules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "change_category",
|
||||
table: "slow_rules",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "failed_import_change_category",
|
||||
table: "queue_cleaner_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "change_category",
|
||||
table: "stall_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "change_category",
|
||||
table: "slow_rules");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "failed_import_change_category",
|
||||
table: "queue_cleaner_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1235,6 +1235,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("ChangeCategory")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_change_category");
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_delete_private");
|
||||
@@ -1275,6 +1279,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("ChangeCategory")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("change_category");
|
||||
|
||||
b.Property<bool>("DeletePrivateTorrentsFromClient")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private_torrents_from_client");
|
||||
@@ -1342,6 +1350,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("ChangeCategory")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("change_category");
|
||||
|
||||
b.Property<bool>("DeletePrivateTorrentsFromClient")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private_torrents_from_client");
|
||||
|
||||
@@ -19,7 +19,9 @@ public sealed record FailedImportConfig
|
||||
public IReadOnlyList<string> Patterns { get; init; } = [];
|
||||
|
||||
public PatternMode PatternMode { get; init; } = PatternMode.Include;
|
||||
|
||||
|
||||
public bool ChangeCategory { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (MaxStrikes is > 0 and < 3)
|
||||
@@ -31,5 +33,10 @@ public sealed record FailedImportConfig
|
||||
{
|
||||
throw new ValidationException("At least one pattern must be specified when using the Include pattern mode");
|
||||
}
|
||||
|
||||
if (ChangeCategory && DeletePrivate)
|
||||
{
|
||||
throw new ValidationException("Cannot enable both deletion and category changing");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,9 @@ public abstract record QueueRule : IConfig, IQueueRule
|
||||
public ushort MaxCompletionPercentage { get; init; } = 100;
|
||||
|
||||
public bool DeletePrivateTorrentsFromClient { get; init; } = false;
|
||||
|
||||
|
||||
public bool ChangeCategory { get; init; } = false;
|
||||
|
||||
public abstract bool MatchesTorrent(ITorrentItemWrapper torrent);
|
||||
|
||||
public virtual void Validate()
|
||||
@@ -62,6 +64,11 @@ public abstract record QueueRule : IConfig, IQueueRule
|
||||
{
|
||||
throw new Cleanuparr.Domain.Exceptions.ValidationException("Maximum completion percentage must be greater than or equal to the minimum completion percentage");
|
||||
}
|
||||
|
||||
if (ChangeCategory && DeletePrivateTorrentsFromClient)
|
||||
{
|
||||
throw new Cleanuparr.Domain.Exceptions.ValidationException("Cannot enable both deletion and category changing");
|
||||
}
|
||||
}
|
||||
|
||||
protected bool MatchesPrivacyType(bool isPrivate)
|
||||
|
||||
@@ -19,6 +19,7 @@ export class DocumentationService {
|
||||
'failedImport.maxStrikes': 'failed-import-max-strikes',
|
||||
'failedImport.ignorePrivate': 'failed-import-ignore-private',
|
||||
'failedImport.deletePrivate': 'failed-import-delete-private',
|
||||
'failedImport.changeCategory': 'failed-import-change-category',
|
||||
'failedImport.skipIfNotFoundInClient': 'failed-import-skip-if-not-found-in-client',
|
||||
'failedImport.pattern-mode': 'failed-import-pattern-mode',
|
||||
'failedImport.patterns': 'failed-import-patterns',
|
||||
@@ -31,6 +32,7 @@ export class DocumentationService {
|
||||
'stallRule.resetStrikesOnProgress': 'stalled-reset-strikes-on-progress',
|
||||
'stallRule.minimumProgress': 'stalled-minimum-progress-to-reset',
|
||||
'stallRule.deletePrivateTorrentsFromClient': 'stalled-delete-private-from-client',
|
||||
'stallRule.changeCategory': 'stalled-change-category',
|
||||
'slowRule.name': 'slow-rule-name',
|
||||
'slowRule.enabled': 'slow-enabled',
|
||||
'slowRule.maxStrikes': 'slow-max-strikes',
|
||||
@@ -41,6 +43,7 @@ export class DocumentationService {
|
||||
'slowRule.ignoreAboveSize': 'slow-ignore-above-size',
|
||||
'slowRule.resetStrikesOnProgress': 'slow-reset-strikes-on-progress',
|
||||
'slowRule.deletePrivateTorrentsFromClient': 'slow-delete-private-from-client',
|
||||
'slowRule.changeCategory': 'slow-change-category',
|
||||
},
|
||||
'general': {
|
||||
'displaySupportBanner': 'display-support-banner',
|
||||
|
||||
@@ -72,10 +72,16 @@
|
||||
[disabled]="failedSubFieldsDisabled()"
|
||||
hint="When enabled, private torrents will not be checked for being failed imports"
|
||||
helpKey="queue-cleaner:failedImport.ignorePrivate" />
|
||||
<app-toggle label="Delete Private from Client" [(checked)]="failedDeletePrivate"
|
||||
[disabled]="failedDeletePrivateDisabled()"
|
||||
hint="Disable this if you want to keep private torrents in the download client even if they are removed from the arrs"
|
||||
helpKey="queue-cleaner:failedImport.deletePrivate" />
|
||||
<app-toggle label="Change category instead of delete" [(checked)]="failedChangeCategory"
|
||||
[disabled]="failedSubFieldsDisabled()"
|
||||
hint="Changes the category to the post-import category set in your arr"
|
||||
helpKey="queue-cleaner:failedImport.changeCategory" />
|
||||
@if (!failedChangeCategory()) {
|
||||
<app-toggle label="Delete Private from Client" [(checked)]="failedDeletePrivate"
|
||||
[disabled]="failedDeletePrivateDisabled()"
|
||||
hint="Disable this if you want to keep private torrents in the download client even if they are removed from the arrs"
|
||||
helpKey="queue-cleaner:failedImport.deletePrivate" />
|
||||
}
|
||||
<app-toggle label="Skip if Not Found in Client" [(checked)]="failedSkipNotFound"
|
||||
[disabled]="failedSubFieldsDisabled()"
|
||||
hint="Skip failed import check for torrents not found in any enabled torrent client"
|
||||
@@ -275,10 +281,15 @@
|
||||
hint="Only reset strikes after the torrent downloads at least this amount. Leave blank to reset on any progress."
|
||||
helpKey="queue-cleaner:stallRule.minimumProgress" />
|
||||
}
|
||||
<app-toggle label="Delete Private from Client" [(checked)]="stallDeletePrivate"
|
||||
[disabled]="stallPrivacyType() === 'Public'"
|
||||
hint="Disable this if you want to keep private torrents in the download client even if they are removed from the arrs"
|
||||
helpKey="queue-cleaner:stallRule.deletePrivateTorrentsFromClient" />
|
||||
<app-toggle label="Change category instead of delete" [(checked)]="stallChangeCategory"
|
||||
hint="Changes the category to the post-import category set in your arr"
|
||||
helpKey="queue-cleaner:stallRule.changeCategory" />
|
||||
@if (!stallChangeCategory()) {
|
||||
<app-toggle label="Delete Private from Client" [(checked)]="stallDeletePrivate"
|
||||
[disabled]="stallPrivacyType() === 'Public'"
|
||||
hint="Disable this if you want to keep private torrents in the download client even if they are removed from the arrs"
|
||||
helpKey="queue-cleaner:stallRule.deletePrivateTorrentsFromClient" />
|
||||
}
|
||||
</div>
|
||||
<div modal-footer>
|
||||
<app-button variant="secondary" (clicked)="stallModalVisible.set(false)">Cancel</app-button>
|
||||
@@ -326,10 +337,15 @@
|
||||
<app-toggle label="Reset Strikes on Progress" [(checked)]="slowResetOnProgress"
|
||||
hint="Reset strike count when torrent shows progress"
|
||||
helpKey="queue-cleaner:slowRule.resetStrikesOnProgress" />
|
||||
<app-toggle label="Delete Private from Client" [(checked)]="slowDeletePrivate"
|
||||
[disabled]="slowPrivacyType() === 'Public'"
|
||||
hint="Disable this if you want to keep private torrents in the download client even if they are removed from the arrs"
|
||||
helpKey="queue-cleaner:slowRule.deletePrivateTorrentsFromClient" />
|
||||
<app-toggle label="Change category instead of delete" [(checked)]="slowChangeCategory"
|
||||
hint="Changes the category to the post-import category set in your arr"
|
||||
helpKey="queue-cleaner:slowRule.changeCategory" />
|
||||
@if (!slowChangeCategory()) {
|
||||
<app-toggle label="Delete Private from Client" [(checked)]="slowDeletePrivate"
|
||||
[disabled]="slowPrivacyType() === 'Public'"
|
||||
hint="Disable this if you want to keep private torrents in the download client even if they are removed from the arrs"
|
||||
helpKey="queue-cleaner:slowRule.deletePrivateTorrentsFromClient" />
|
||||
}
|
||||
</div>
|
||||
<div modal-footer>
|
||||
<app-button variant="secondary" (clicked)="slowModalVisible.set(false)">Cancel</app-button>
|
||||
|
||||
@@ -98,6 +98,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
readonly failedSkipNotFound = signal(false);
|
||||
readonly failedPatterns = signal<string[]>([]);
|
||||
readonly failedPatternMode = signal<unknown>(PatternMode.Exclude);
|
||||
readonly failedChangeCategory = signal(false);
|
||||
readonly failedExpanded = signal(true);
|
||||
|
||||
// Metadata
|
||||
@@ -121,6 +122,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
readonly stallResetOnProgress = signal(false);
|
||||
readonly stallMinProgress = signal('');
|
||||
readonly stallDeletePrivate = signal(false);
|
||||
readonly stallChangeCategory = signal(false);
|
||||
|
||||
// Slow rules
|
||||
readonly slowRules = signal<SlowRule[]>([]);
|
||||
@@ -141,6 +143,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
readonly slowIgnoreAboveSize = signal('');
|
||||
readonly slowResetOnProgress = signal(false);
|
||||
readonly slowDeletePrivate = signal(false);
|
||||
readonly slowChangeCategory = signal(false);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
@@ -158,6 +161,24 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
untracked(() => this.failedDeletePrivate.set(false));
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (this.failedChangeCategory()) {
|
||||
untracked(() => this.failedDeletePrivate.set(false));
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (this.stallChangeCategory()) {
|
||||
untracked(() => this.stallDeletePrivate.set(false));
|
||||
}
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
if (this.slowChangeCategory()) {
|
||||
untracked(() => this.slowDeletePrivate.set(false));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Validation
|
||||
@@ -300,6 +321,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.failedSkipNotFound.set(config.failedImport.skipIfNotFoundInClient);
|
||||
this.failedPatterns.set(config.failedImport.patterns ?? []);
|
||||
this.failedPatternMode.set(config.failedImport.patternMode ?? PatternMode.Exclude);
|
||||
this.failedChangeCategory.set(config.failedImport.changeCategory ?? false);
|
||||
this.metadataMaxStrikes.set(config.downloadingMetadataMaxStrikes);
|
||||
this.loader.stop();
|
||||
this.savedSnapshot.set(this.buildSnapshot());
|
||||
@@ -360,6 +382,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.stallResetOnProgress.set(rule.resetStrikesOnProgress);
|
||||
this.stallMinProgress.set(rule.minimumProgress ?? '');
|
||||
this.stallDeletePrivate.set(rule.deletePrivateTorrentsFromClient);
|
||||
this.stallChangeCategory.set(rule.changeCategory ?? false);
|
||||
} else {
|
||||
this.stallName.set('');
|
||||
this.stallEnabled.set(true);
|
||||
@@ -370,6 +393,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.stallResetOnProgress.set(false);
|
||||
this.stallMinProgress.set('');
|
||||
this.stallDeletePrivate.set(false);
|
||||
this.stallChangeCategory.set(false);
|
||||
}
|
||||
this.stallModalVisible.set(true);
|
||||
}
|
||||
@@ -377,6 +401,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
saveStallRule(): void {
|
||||
if (this.stallNameError() || this.stallMaxStrikesError() || this.stallCompletionError()) return;
|
||||
|
||||
const changeCategory = this.stallChangeCategory();
|
||||
const dto: CreateStallRuleDto = {
|
||||
name: this.stallName().trim(),
|
||||
enabled: this.stallEnabled(),
|
||||
@@ -386,7 +411,8 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
maxCompletionPercentage: this.stallMaxCompletion() ?? 100,
|
||||
resetStrikesOnProgress: this.stallResetOnProgress(),
|
||||
minimumProgress: this.stallMinProgress().trim() || null,
|
||||
deletePrivateTorrentsFromClient: this.stallDeletePrivate(),
|
||||
deletePrivateTorrentsFromClient: changeCategory ? false : this.stallDeletePrivate(),
|
||||
changeCategory,
|
||||
};
|
||||
|
||||
const editing = this.editingStallRule();
|
||||
@@ -436,6 +462,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.slowIgnoreAboveSize.set(rule.ignoreAboveSize ?? '');
|
||||
this.slowResetOnProgress.set(rule.resetStrikesOnProgress);
|
||||
this.slowDeletePrivate.set(rule.deletePrivateTorrentsFromClient);
|
||||
this.slowChangeCategory.set(rule.changeCategory ?? false);
|
||||
} else {
|
||||
this.slowName.set('');
|
||||
this.slowEnabled.set(true);
|
||||
@@ -448,6 +475,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
this.slowIgnoreAboveSize.set('');
|
||||
this.slowResetOnProgress.set(false);
|
||||
this.slowDeletePrivate.set(false);
|
||||
this.slowChangeCategory.set(false);
|
||||
}
|
||||
this.slowModalVisible.set(true);
|
||||
}
|
||||
@@ -455,6 +483,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
saveSlowRule(): void {
|
||||
if (this.slowNameError() || this.slowMaxStrikesError() || this.slowCompletionError()) return;
|
||||
|
||||
const changeCategory = this.slowChangeCategory();
|
||||
const dto: CreateSlowRuleDto = {
|
||||
name: this.slowName().trim(),
|
||||
enabled: this.slowEnabled(),
|
||||
@@ -466,7 +495,8 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
minSpeed: this.slowMinSpeed().trim(),
|
||||
maxTimeHours: this.slowMaxTimeHours() ?? 0,
|
||||
ignoreAboveSize: this.slowIgnoreAboveSize().trim() || undefined,
|
||||
deletePrivateTorrentsFromClient: this.slowDeletePrivate(),
|
||||
deletePrivateTorrentsFromClient: changeCategory ? false : this.slowDeletePrivate(),
|
||||
changeCategory,
|
||||
};
|
||||
|
||||
const editing = this.editingSlowRule();
|
||||
@@ -519,10 +549,11 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
failedImport: {
|
||||
maxStrikes: this.failedMaxStrikes() ?? 3,
|
||||
ignorePrivate: this.failedIgnorePrivate(),
|
||||
deletePrivate: this.failedDeletePrivate(),
|
||||
deletePrivate: this.failedChangeCategory() ? false : this.failedDeletePrivate(),
|
||||
skipIfNotFoundInClient: this.failedSkipNotFound(),
|
||||
patterns: this.failedPatterns(),
|
||||
patternMode: this.failedPatternMode() as PatternMode,
|
||||
changeCategory: this.failedChangeCategory(),
|
||||
},
|
||||
downloadingMetadataMaxStrikes: this.metadataMaxStrikes() ?? 3,
|
||||
};
|
||||
@@ -558,6 +589,7 @@ export class QueueCleanerComponent implements OnInit, HasPendingChanges {
|
||||
failedSkipNotFound: this.failedSkipNotFound(),
|
||||
failedPatterns: this.failedPatterns(),
|
||||
failedPatternMode: this.failedPatternMode(),
|
||||
failedChangeCategory: this.failedChangeCategory(),
|
||||
metadataMaxStrikes: this.metadataMaxStrikes(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface FailedImportConfig {
|
||||
skipIfNotFoundInClient: boolean;
|
||||
patterns: string[];
|
||||
patternMode?: PatternMode;
|
||||
changeCategory: boolean;
|
||||
}
|
||||
|
||||
export interface QueueCleanerConfig {
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface QueueRule {
|
||||
minCompletionPercentage: number;
|
||||
maxCompletionPercentage: number;
|
||||
deletePrivateTorrentsFromClient: boolean;
|
||||
changeCategory: boolean;
|
||||
}
|
||||
|
||||
export interface StallRule extends QueueRule {
|
||||
@@ -32,6 +33,7 @@ export interface CreateStallRuleDto {
|
||||
maxCompletionPercentage: number;
|
||||
resetStrikesOnProgress: boolean;
|
||||
deletePrivateTorrentsFromClient: boolean;
|
||||
changeCategory: boolean;
|
||||
minimumProgress?: string | null;
|
||||
}
|
||||
|
||||
@@ -47,4 +49,5 @@ export interface CreateSlowRuleDto {
|
||||
maxTimeHours: number;
|
||||
ignoreAboveSize?: string;
|
||||
deletePrivateTorrentsFromClient: boolean;
|
||||
changeCategory: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user