mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
Deduplicate download client logic (#812)
This commit is contained in:
1 parent
124fcbe747
commit
aff3880a0d
38 files changed
+2268
-1068
No files matched your search
+76
@@ -749,6 +749,82 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
await _fixture.EventPublisher.DidNotReceive().PublishCategoryChanged(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NullContents_TreatedAsNoFiles_NoHardlinks_ChangesLabel()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var unlinkedConfig = new UnlinkedConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TargetCategory = "unlinked"
|
||||
};
|
||||
|
||||
var downloads = new List<Domain.Entities.ITorrentItemWrapper>
|
||||
{
|
||||
new DelugeItemWrapper(new DownloadStatus { Hash = "hash1", Name = "Test", Label = "movies", Trackers = new List<Tracker>(), DownloadLocation = "/downloads" })
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles("hash1")
|
||||
.Returns(new DelugeContents { Contents = null });
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabel("hash1", "unlinked");
|
||||
_fixture.HardLinkFileService
|
||||
.DidNotReceive()
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagTrue_StillChangesLabel_AndPublishesEventWithIsTagFalse()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var unlinkedConfig = new UnlinkedConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TargetCategory = "unlinked",
|
||||
UseTag = true
|
||||
};
|
||||
|
||||
var downloads = new List<Domain.Entities.ITorrentItemWrapper>
|
||||
{
|
||||
new DelugeItemWrapper(new DownloadStatus { Hash = "hash1", Name = "Test", Label = "movies", Trackers = new List<Tracker>(), DownloadLocation = "/downloads" })
|
||||
};
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles("hash1")
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "file1.mkv", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "file1.mkv" } }
|
||||
}
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabel("hash1", "unlinked");
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "unlinked", false);
|
||||
downloads[0].Category.ShouldBe("unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+370
-3
@@ -8,6 +8,7 @@ using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Deluge;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
@@ -590,6 +591,77 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeTorrentCategoryAsync_Tests : DelugeServiceTests
|
||||
{
|
||||
public ChangeTorrentCategoryAsync_Tests(DelugeServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagFalse_ChangesLabel_PublishesCategoryEvent_UpdatesTorrentCategory()
|
||||
{
|
||||
var sut = _fixture.CreateSut();
|
||||
var torrent = new DelugeItemWrapper(new DownloadStatus
|
||||
{
|
||||
Hash = "hash1",
|
||||
Name = "Test",
|
||||
Label = "movies",
|
||||
Trackers = new List<Tracker>(),
|
||||
DownloadLocation = "/downloads"
|
||||
});
|
||||
|
||||
await sut.ChangeTorrentCategoryAsync(torrent, "target", false);
|
||||
|
||||
await _fixture.ClientWrapper.Received(1).SetTorrentLabel("hash1", "target");
|
||||
await _fixture.EventPublisher.Received(1).PublishCategoryChanged("movies", "target", false);
|
||||
torrent.Category.ShouldBe("target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagTrue_BehavesLikeUseTagFalse_BecauseDelugeHasNoTags()
|
||||
{
|
||||
var sut = _fixture.CreateSut();
|
||||
var torrent = new DelugeItemWrapper(new DownloadStatus
|
||||
{
|
||||
Hash = "hash1",
|
||||
Name = "Test",
|
||||
Label = "movies",
|
||||
Trackers = new List<Tracker>(),
|
||||
DownloadLocation = "/downloads"
|
||||
});
|
||||
|
||||
await sut.ChangeTorrentCategoryAsync(torrent, "target", true);
|
||||
|
||||
await _fixture.ClientWrapper.Received(1).SetTorrentLabel("hash1", "target");
|
||||
await _fixture.EventPublisher.Received(1).PublishCategoryChanged("movies", "target", false);
|
||||
torrent.Category.ShouldBe("target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_SkipsLabelChange_ButStillPublishesEventAndUpdatesCategory()
|
||||
{
|
||||
var sut = _fixture.CreateSut();
|
||||
var torrent = new DelugeItemWrapper(new DownloadStatus
|
||||
{
|
||||
Hash = "hash1",
|
||||
Name = "Test",
|
||||
Label = "movies",
|
||||
Trackers = new List<Tracker>(),
|
||||
DownloadLocation = "/downloads"
|
||||
});
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await sut.ChangeTorrentCategoryAsync(torrent, "target", false);
|
||||
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetTorrentLabel(Arg.Any<string>(), Arg.Any<string>());
|
||||
await _fixture.EventPublisher.Received(1).PublishCategoryChanged("movies", "target", false);
|
||||
torrent.Category.ShouldBe("target");
|
||||
}
|
||||
}
|
||||
|
||||
public class BlockUnwantedFilesAsyncScenarios : DelugeServiceTests
|
||||
{
|
||||
public BlockUnwantedFilesAsyncScenarios(DelugeServiceFixture fixture) : base(fixture)
|
||||
@@ -624,7 +696,7 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_DoesNotCallChangeFilesPriority_AndMarksForRemoval()
|
||||
public async Task AllFilesAreMalware_CallsChangeFilesPriority_AndMarksForRemoval()
|
||||
{
|
||||
const string hash = "all-malware-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
@@ -655,8 +727,48 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.ChangeFilesPriority(Arg.Any<string>(), Arg.Any<List<int>>());
|
||||
.Received(1)
|
||||
.ChangeFilesPriority(hash, Arg.Is<List<int>>(p => p.Count == 1 && p[0] == 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_PriorityUpdateThrows_StillMarksForRemoval()
|
||||
{
|
||||
const string hash = "all-malware-priority-throws-hash";
|
||||
DelugeService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "malware.exe", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "malware.exe" } },
|
||||
},
|
||||
});
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.ChangeFilesPriority(hash, Arg.Is<List<int>>(p => p.Count == 1 && p[0] == 0))
|
||||
.ThrowsAsync(new HttpRequestException("rejected"));
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.ChangeFilesPriority(hash, Arg.Is<List<int>>(p => p.Count == 1 && p[0] == 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -738,5 +850,260 @@ public class DelugeServiceTests : IClassFixture<DelugeServiceFixture>
|
||||
.DidNotReceive()
|
||||
.ChangeFilesPriority(Arg.Any<string>(), Arg.Any<List<int>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NoUnwantedFiles_DoesNotCallChangeFilesPriority_AndDoesNotRemove()
|
||||
{
|
||||
const string hash = "no-unwanted-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "movie.mkv", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "movie.mkv" } },
|
||||
},
|
||||
});
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.ChangeFilesPriority(Arg.Any<string>(), Arg.Any<List<int>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AlreadySkippedFilePlusMalwareFile_MarksAllFilesBlocked()
|
||||
{
|
||||
const string hash = "skipped-plus-malware-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "skipped.txt", new DelugeFileOrDirectory { Type = "file", Priority = 0, Index = 0, Path = "skipped.txt" } },
|
||||
{ "malware.exe", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 1, Path = "malware.exe" } },
|
||||
},
|
||||
});
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteIfAnyFileBlocked_AlreadySkippedFileAlone_DoesNotTriggerAtLeastOneFileBlocked()
|
||||
{
|
||||
const string hash = "skipped-alone-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { DeleteIfAnyFileBlocked = true });
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "skipped.txt", new DelugeFileOrDirectory { Type = "file", Priority = 0, Index = 0, Path = "skipped.txt" } },
|
||||
},
|
||||
});
|
||||
|
||||
var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.ChangeFilesPriority(Arg.Any<string>(), Arg.Any<List<int>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_NestedDirectory_ChangeFilesPriorityReceivesFullVectorWithOriginalPrioritiesPreserved()
|
||||
{
|
||||
const string hash = "nested-partial-malware-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "movie.mkv", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "movie.mkv" } },
|
||||
{
|
||||
"Subs", new DelugeFileOrDirectory
|
||||
{
|
||||
Type = "dir",
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "malware.exe", new DelugeFileOrDirectory { Type = "file", Priority = 5, Index = 1, Path = "Subs/malware.exe" } },
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "sample.mkv", new DelugeFileOrDirectory { Type = "file", Priority = 7, Index = 2, Path = "sample.mkv" } },
|
||||
},
|
||||
});
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name == "malware.exe"), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name != "malware.exe"), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.ChangeFilesPriority(hash, Arg.Is<List<int>>(p => p.Count == 3 && p[0] == 1 && p[1] == 0 && p[2] == 7));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IgnorePrivate_PrivateTorrent_SkipsFileCheck_EvaluatorNeverCalled()
|
||||
{
|
||||
const string hash = "private-ignore-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { IgnorePrivate = true });
|
||||
|
||||
DownloadStatus downloadStatus = MakeDownloadStatus(hash) with { Private = true };
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(downloadStatus);
|
||||
|
||||
var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.IsPrivate.ShouldBeTrue();
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.DidNotReceive()
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.ChangeFilesPriority(Arg.Any<string>(), Arg.Any<List<int>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_StillSetsAllFilesBlockedVerdict_ButSkipsChangeFilesPriority()
|
||||
{
|
||||
const string hash = "dry-run-all-malware-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "malware.exe", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "malware.exe" } },
|
||||
},
|
||||
});
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.ChangeFilesPriority(Arg.Any<string>(), Arg.Any<List<int>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NestedFile_ValidatesBareFileName_NotRelativePath()
|
||||
{
|
||||
const string hash = "nested-bare-name-hash";
|
||||
var sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentStatus(hash)
|
||||
.Returns(MakeDownloadStatus(hash));
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFiles(hash)
|
||||
.Returns(new DelugeContents
|
||||
{
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{
|
||||
"Season 01", new DelugeFileOrDirectory
|
||||
{
|
||||
Type = "dir",
|
||||
Contents = new Dictionary<string, DelugeFileOrDirectory>
|
||||
{
|
||||
{ "episode.mkv", new DelugeFileOrDirectory { Type = "file", Priority = 1, Index = 0, Path = "Season 01/episode.mkv" } },
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.Received(1)
|
||||
.IsValid("episode.mkv", Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
_fixture.FilenameEvaluator
|
||||
.DidNotReceive()
|
||||
.IsValid("Season 01/episode.mkv", Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -1229,6 +1229,29 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "cleanuparr-dead", true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_SkipsClientCall_ButPublishesEvent()
|
||||
{
|
||||
var sut = _fixture.CreateSut();
|
||||
var torrent = Substitute.For<ITorrentItemWrapper>();
|
||||
torrent.Hash.Returns("hash1");
|
||||
torrent.Name.Returns("Test");
|
||||
torrent.Category.Returns("movies");
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await sut.ChangeTorrentCategoryAsync(torrent, "cleanuparr-dead", useTag: false);
|
||||
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetTorrentCategoryAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.AddTorrentTagAsync(Arg.Any<IEnumerable<string>>(), Arg.Any<string>());
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "cleanuparr-dead", false);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeCategoryForNoHardLinksAsync_Tests : QBitServiceDCTests
|
||||
|
||||
+123
@@ -7,6 +7,7 @@ using Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using QBittorrent.Client;
|
||||
using Shouldly;
|
||||
@@ -1371,6 +1372,41 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilePriorityAsync(hash, 0, TorrentContentPriority.Skip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_PriorityUpdateThrows_StillMarksForRemoval()
|
||||
{
|
||||
const string hash = "all-malware-priority-throws-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "malware.exe", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.SetFilePriorityAsync(hash, 0, TorrentContentPriority.Skip)
|
||||
.ThrowsAsync(new HttpRequestException("rejected"));
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilePriorityAsync(hash, 0, TorrentContentPriority.Skip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1574,5 +1610,92 @@ public class QBitServiceTests : IClassFixture<QBitServiceFixture>
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IgnorePrivate_PrivateTorrent_SkipsFileCheck()
|
||||
{
|
||||
const string hash = "private-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { IgnorePrivate = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "malware.exe", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
], isPrivate: true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.IsPrivate.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<TorrentContentPriority>());
|
||||
_fixture.FilenameEvaluator
|
||||
.DidNotReceive()
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FileWithNullIndex_ExcludedFromTotals_MalwareFileStillMarksAllFilesBlocked()
|
||||
{
|
||||
const string hash = "null-index-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "unindexed.mkv", Index = null, Priority = TorrentContentPriority.Normal },
|
||||
new TorrentContent { Name = "malware.exe", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilePriorityAsync(hash, 0, TorrentContentPriority.Skip);
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(hash, Arg.Is<int>(i => i != 0), Arg.Any<TorrentContentPriority>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_AllFilesMalware_VerdictSet_ButNoSetFilePriorityCall()
|
||||
{
|
||||
const string hash = "dry-run-hash";
|
||||
QBitService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new TorrentContent { Name = "malware.exe", Index = 0, Priority = TorrentContentPriority.Normal },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<TorrentContentPriority>());
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -528,6 +528,71 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeTorrentCategoryAsync_Tests : RTorrentServiceDCTests
|
||||
{
|
||||
public ChangeTorrentCategoryAsync_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagFalse_ChangesLabel_PublishesEvent_UpdatesCategory()
|
||||
{
|
||||
// Arrange
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
RTorrentItemWrapper wrapper = new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies" }, null, TimeProvider.System);
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(wrapper, "target", useTag: false);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetLabelAsync("HASH1", "target");
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "target", false);
|
||||
wrapper.Category.ShouldBe("target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagTrue_BehavesLikeUseTagFalse_BecauseRTorrentDoesNotSupportTags()
|
||||
{
|
||||
// Arrange
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
RTorrentItemWrapper wrapper = new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies" }, null, TimeProvider.System);
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(wrapper, "target", useTag: true);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetLabelAsync("HASH1", "target");
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "target", false);
|
||||
wrapper.Category.ShouldBe("target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_SkipsLabelCall_ButPublishesEventAndUpdatesCategory()
|
||||
{
|
||||
// Arrange
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
RTorrentItemWrapper wrapper = new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies" }, null, TimeProvider.System);
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(wrapper, "target", useTag: false);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "target", false);
|
||||
wrapper.Category.ShouldBe("target");
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeCategoryForNoHardLinksAsync_Tests : RTorrentServiceDCTests
|
||||
{
|
||||
public ChangeCategoryForNoHardLinksAsync_Tests(RTorrentServiceFixture fixture) : base(fixture)
|
||||
@@ -859,6 +924,44 @@ public class RTorrentServiceDCTests : IClassFixture<RTorrentServiceFixture>
|
||||
.PublishCategoryChanged("movies", "unlinked", false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagTrue_ChangesLabel_PublishesEventWithIsTagFalse_UpdatesCategory()
|
||||
{
|
||||
// Arrange
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
|
||||
UnlinkedConfig unlinkedConfig = new UnlinkedConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TargetCategory = "unlinked",
|
||||
UseTag = true
|
||||
};
|
||||
|
||||
RTorrentItemWrapper wrapper = new RTorrentItemWrapper(new RTorrentTorrent { Hash = "HASH1", Name = "Test", Label = "movies", BasePath = "/downloads" }, null, TimeProvider.System);
|
||||
List<ITorrentItemWrapper> downloads = new List<ITorrentItemWrapper> { wrapper };
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFilesAsync("HASH1")
|
||||
.Returns(new List<RTorrentFile>
|
||||
{
|
||||
new RTorrentFile { Index = 0, Path = "file1.mkv", Priority = 1 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - rTorrent does not support tags, so useTag falls back to a category change
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetLabelAsync("HASH1", "unlinked");
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "unlinked", false);
|
||||
wrapper.Category.ShouldBe("unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UsesDirectoryOverBasePathForFilePath()
|
||||
{
|
||||
|
||||
+134
@@ -833,6 +833,38 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilePriorityAsync(hash, 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_PriorityUpdateThrows_StillMarksForRemoval()
|
||||
{
|
||||
const string hash = "ALL-MALWARE-PRIORITY-THROWS-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new RTorrentFile { Index = 0, Path = "malware.exe", Priority = 1 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.SetFilePriorityAsync(hash, 0, 0)
|
||||
.ThrowsAsync(new HttpRequestException("rejected"));
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilePriorityAsync(hash, 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -944,5 +976,107 @@ public class RTorrentServiceTests : IClassFixture<RTorrentServiceFixture>
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrivateTorrent_WithIgnorePrivate_SkipsFileCheck_NeverEvaluatesFilenames()
|
||||
{
|
||||
const string hash = "IGNORE-PRIVATE-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { IgnorePrivate = true });
|
||||
|
||||
StubClient(hash, [new RTorrentFile { Index = 0, Path = "installer.exe", Priority = 1 }], isPrivate: true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.IsPrivate.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.DidNotReceive()
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_AllFilesMalware_StillMarksForRemoval_SkipsPriorityCall()
|
||||
{
|
||||
const string hash = "DRY-RUN-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new RTorrentFile { Index = 0, Path = "malware.exe", Priority = 1 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilePriorityAsync(Arg.Any<string>(), Arg.Any<int>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_MultipleUnwantedFiles_CallsSetFilePriorityForExactIndicesOnly()
|
||||
{
|
||||
const string hash = "MULTI-PARTIAL-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new RTorrentFile { Index = 0, Path = "movie.mkv", Priority = 1 },
|
||||
new RTorrentFile { Index = 1, Path = "malware1.exe", Priority = 1 },
|
||||
new RTorrentFile { Index = 2, Path = "malware2.exe", Priority = 1 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.StartsWith("malware")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
|
||||
await _fixture.ClientWrapper.Received(1).SetFilePriorityAsync(hash, 1, 0);
|
||||
await _fixture.ClientWrapper.Received(1).SetFilePriorityAsync(hash, 2, 0);
|
||||
await _fixture.ClientWrapper.DidNotReceive().SetFilePriorityAsync(hash, 0, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NestedFilePath_ValidatesBareFilename()
|
||||
{
|
||||
const string hash = "NESTED-PATH-HASH";
|
||||
RTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new RTorrentFile { Index = 0, Path = "Season 01/Episode.01.mkv", Priority = 1 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.Received(1)
|
||||
.IsValid("Episode.01.mkv", Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -580,6 +580,106 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeTorrentCategoryAsync_Tests : TransmissionServiceDCTests
|
||||
{
|
||||
public ChangeTorrentCategoryAsync_Tests(TransmissionServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CategoryMode_ChangesLocation_PublishesEvent_AndUpdatesCategory()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var baseDownloadDir = Path.Combine("downloads", "movies");
|
||||
var expectedNewLocation = string.Join(Path.DirectorySeparatorChar,
|
||||
Path.Combine(baseDownloadDir, "cleanuparr-dead").Split(['\\', '/']));
|
||||
|
||||
var torrent = new TransmissionItemWrapper(new TorrentInfo
|
||||
{
|
||||
Id = 123,
|
||||
HashString = "hash1",
|
||||
Name = "Test",
|
||||
DownloadDir = baseDownloadDir
|
||||
});
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(torrent, "cleanuparr-dead", useTag: false);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentSetLocationAsync(Arg.Is<long[]>(ids => ids.Contains(123)), expectedNewLocation, true);
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "cleanuparr-dead", false);
|
||||
torrent.Category.ShouldBe("cleanuparr-dead");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TagMode_SetsLabels_PublishesEvent_AndKeepsCategoryUnchanged()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
var baseDownloadDir = Path.Combine("downloads", "movies");
|
||||
|
||||
var torrent = new TransmissionItemWrapper(new TorrentInfo
|
||||
{
|
||||
Id = 123,
|
||||
HashString = "hash1",
|
||||
Name = "Test",
|
||||
DownloadDir = baseDownloadDir,
|
||||
Labels = ["existing", "CLEANUPARR-DEAD"]
|
||||
});
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(torrent, "cleanuparr-dead", useTag: true);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.TorrentSetAsync(Arg.Is<TorrentSettings>(s =>
|
||||
s.Ids.Contains(123L)
|
||||
&& s.Labels.Contains("existing")
|
||||
&& s.Labels.Contains("CLEANUPARR-DEAD")
|
||||
&& s.Labels.Length == 2));
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "cleanuparr-dead", true);
|
||||
torrent.Category.ShouldBe("movies");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_SkipsClientCall_ButPublishesEvent()
|
||||
{
|
||||
// Arrange
|
||||
var sut = _fixture.CreateSut();
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var torrent = new TransmissionItemWrapper(new TorrentInfo
|
||||
{
|
||||
Id = 123,
|
||||
HashString = "hash1",
|
||||
Name = "Test",
|
||||
DownloadDir = Path.Combine("downloads", "movies")
|
||||
});
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(torrent, "cleanuparr-dead", useTag: false);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.TorrentSetLocationAsync(Arg.Any<long[]>(), Arg.Any<string>(), Arg.Any<bool>());
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "cleanuparr-dead", false);
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeCategoryForNoHardLinksAsync_Tests : TransmissionServiceDCTests
|
||||
{
|
||||
public ChangeCategoryForNoHardLinksAsync_Tests(TransmissionServiceFixture fixture) : base(fixture)
|
||||
|
||||
+113
@@ -6,6 +6,7 @@ using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
using Shouldly;
|
||||
@@ -592,6 +593,38 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_PriorityUpdateThrows_StillMarksForRemoval()
|
||||
{
|
||||
const string hash = "all-malware-priority-throws-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [("malware.exe", true)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>())
|
||||
.ThrowsAsync(new HttpRequestException("rejected"));
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -691,5 +724,85 @@ public class TransmissionServiceTests : IClassFixture<TransmissionServiceFixture
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IgnorePrivate_PrivateTorrent_SkipsFileCheck()
|
||||
{
|
||||
const string hash = "private-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { IgnorePrivate = true });
|
||||
|
||||
StubClient(hash, [("malware.exe", true)], isPrivate: true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.IsPrivate.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
_fixture.FilenameEvaluator
|
||||
.DidNotReceive()
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_TorrentSetContainsExactUnwantedIndices()
|
||||
{
|
||||
const string hash = "exact-indices-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [("movie.mkv", true), ("installer.exe", true), ("readme.txt", true)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("installer.exe")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("readme.txt")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
|
||||
await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.TorrentSetAsync(Arg.Is<TorrentSettings>(s =>
|
||||
s.FilesUnwanted != null
|
||||
&& s.FilesUnwanted.Length == 2
|
||||
&& s.FilesUnwanted.Contains(1L)
|
||||
&& s.FilesUnwanted.Contains(2L)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_AllFilesMalware_VerdictSet_ButNoTorrentSetCall()
|
||||
{
|
||||
const string hash = "dry-run-hash";
|
||||
TransmissionService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [("malware.exe", true)]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.TorrentSetAsync(Arg.Any<TorrentSettings>());
|
||||
}
|
||||
}
|
||||
}
|
||||
+111
@@ -515,6 +515,77 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeTorrentCategoryAsync_Tests : UTorrentServiceDCTests
|
||||
{
|
||||
public ChangeTorrentCategoryAsync_Tests(UTorrentServiceFixture fixture) : base(fixture)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagFalse_ChangesLabel_PublishesEvent_UpdatesCategory()
|
||||
{
|
||||
// Arrange
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
UTorrentItemWrapper wrapper = new UTorrentItemWrapper(
|
||||
new UTorrentItem { Hash = "hash1", Name = "Test", Label = "movies" },
|
||||
new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" }, TimeProvider.System);
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(wrapper, "target", useTag: false);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabelAsync("hash1", "target");
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "target", false);
|
||||
wrapper.Category.ShouldBe("target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagTrue_BehavesLikeUseTagFalse_BecauseUTorrentDoesNotSupportTags()
|
||||
{
|
||||
// Arrange
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
UTorrentItemWrapper wrapper = new UTorrentItemWrapper(
|
||||
new UTorrentItem { Hash = "hash1", Name = "Test", Label = "movies" },
|
||||
new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" }, TimeProvider.System);
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(wrapper, "target", useTag: true);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabelAsync("hash1", "target");
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "target", false);
|
||||
wrapper.Category.ShouldBe("target");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_SkipsLabelCall_ButPublishesEventAndUpdatesCategory()
|
||||
{
|
||||
// Arrange
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
UTorrentItemWrapper wrapper = new UTorrentItemWrapper(
|
||||
new UTorrentItem { Hash = "hash1", Name = "Test", Label = "movies" },
|
||||
new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" }, TimeProvider.System);
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.ChangeTorrentCategoryAsync(wrapper, "target", useTag: false);
|
||||
|
||||
// Assert
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetTorrentLabelAsync(Arg.Any<string>(), Arg.Any<string>());
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "target", false);
|
||||
wrapper.Category.ShouldBe("target");
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeCategoryForNoHardLinksAsync_Tests : UTorrentServiceDCTests
|
||||
{
|
||||
public ChangeCategoryForNoHardLinksAsync_Tests(UTorrentServiceFixture fixture) : base(fixture)
|
||||
@@ -826,6 +897,46 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
.SetTorrentLabelAsync("hash1", "unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UseTagTrue_ChangesLabel_PublishesEventWithIsTagFalse_UpdatesCategory()
|
||||
{
|
||||
// Arrange
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
|
||||
UnlinkedConfig unlinkedConfig = new UnlinkedConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TargetCategory = "unlinked",
|
||||
UseTag = true
|
||||
};
|
||||
|
||||
UTorrentItemWrapper wrapper = new UTorrentItemWrapper(
|
||||
new UTorrentItem { Hash = "hash1", Name = "Test", Label = "movies", SavePath = "/downloads" },
|
||||
new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" }, TimeProvider.System);
|
||||
List<Domain.Entities.ITorrentItemWrapper> downloads = new List<Domain.Entities.ITorrentItemWrapper> { wrapper };
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.GetTorrentFilesAsync("hash1")
|
||||
.Returns(new List<UTorrentFile>
|
||||
{
|
||||
new UTorrentFile { Name = "file1.mkv", Priority = 1, Index = 0, Size = 1000, Downloaded = 500 }
|
||||
});
|
||||
|
||||
_fixture.HardLinkFileService
|
||||
.GetHardLinkCount(Arg.Any<string>(), Arg.Any<bool>())
|
||||
.Returns(0);
|
||||
|
||||
// Act
|
||||
await sut.ChangeCategoryForNoHardLinksAsync(downloads, unlinkedConfig);
|
||||
|
||||
// Assert - uTorrent does not support tags, so useTag falls back to a category change
|
||||
await _fixture.ClientWrapper.Received(1)
|
||||
.SetTorrentLabelAsync("hash1", "unlinked");
|
||||
_fixture.EventPublisher.Received(1)
|
||||
.PublishCategoryChanged("movies", "unlinked", false);
|
||||
wrapper.Category.ShouldBe("unlinked");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NullFilesResponse_ChangesLabel()
|
||||
{
|
||||
|
||||
+116
@@ -799,6 +799,38 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilesPriorityAsync(hash, Arg.Is<List<int>>(idx => idx.Count == 1 && idx[0] == 0), 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AllFilesAreMalware_PriorityUpdateThrows_StillMarksForRemoval()
|
||||
{
|
||||
const string hash = "all-malware-priority-throws-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new UTorrentFile { Name = "malware.exe", Index = 0, Priority = 2, Size = 1024, Downloaded = 1024 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.SetFilesPriorityAsync(hash, Arg.Is<List<int>>(idx => idx.Count == 1 && idx[0] == 0), 0)
|
||||
.ThrowsAsync(new HttpRequestException("rejected"));
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilesPriorityAsync(hash, Arg.Is<List<int>>(idx => idx.Count == 1 && idx[0] == 0), 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1009,5 +1041,89 @@ public class UTorrentServiceTests : IClassFixture<UTorrentServiceFixture>
|
||||
await _fixture.ClientWrapper.DidNotReceive()
|
||||
.SetFilesPriorityAsync(Arg.Any<string>(), Arg.Any<List<int>>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrivateTorrent_WithIgnorePrivate_NeverEvaluatesFilenames()
|
||||
{
|
||||
const string hash = "ignore-private-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext(new ContentBlockerConfig { IgnorePrivate = true });
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new UTorrentFile { Name = "installer.exe", Index = 0, Priority = 2, Size = 1024, Downloaded = 1024 },
|
||||
], isPrivate: true);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.IsPrivate.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.DidNotReceive()
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>());
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilesPriorityAsync(Arg.Any<string>(), Arg.Any<List<int>>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DryRun_AllFilesMalware_StillMarksForRemoval_SkipsPriorityCall()
|
||||
{
|
||||
const string hash = "dry-run-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash, [new UTorrentFile { Name = "malware.exe", Index = 0, Priority = 2, Size = 1024, Downloaded = 1024 }]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Any<string>(), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
_fixture.DryRunInterceptor
|
||||
.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.Found.ShouldBeTrue();
|
||||
result.ShouldRemove.ShouldBeTrue();
|
||||
result.DeleteReason.ShouldBe(DeleteReason.AllFilesBlocked);
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.DidNotReceive()
|
||||
.SetFilesPriorityAsync(Arg.Any<string>(), Arg.Any<List<int>>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PartialMalware_MultipleUnwantedFiles_CallsSetFilesPriorityWithExactIndicesOnly()
|
||||
{
|
||||
const string hash = "multi-partial-hash";
|
||||
UTorrentService sut = _fixture.CreateSut();
|
||||
SetMalwareBlockerContext();
|
||||
|
||||
StubClient(hash,
|
||||
[
|
||||
new UTorrentFile { Name = "movie.mkv", Index = 0, Priority = 2, Size = 32_768, Downloaded = 32_768 },
|
||||
new UTorrentFile { Name = "malware1.exe", Index = 1, Priority = 2, Size = 1024, Downloaded = 1024 },
|
||||
new UTorrentFile { Name = "malware2.exe", Index = 2, Priority = 2, Size = 1024, Downloaded = 1024 },
|
||||
]);
|
||||
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.EndsWith("movie.mkv")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(true);
|
||||
_fixture.FilenameEvaluator
|
||||
.IsValid(Arg.Is<string>(name => name.StartsWith("malware")), Arg.Any<BlocklistType>(), Arg.Any<ConcurrentBag<string>>(), Arg.Any<ConcurrentBag<Regex>>())
|
||||
.Returns(false);
|
||||
|
||||
BlockFilesResult result = await sut.BlockUnwantedFilesAsync(hash, Array.Empty<string>());
|
||||
|
||||
result.ShouldRemove.ShouldBeFalse();
|
||||
|
||||
await _fixture.ClientWrapper
|
||||
.Received(1)
|
||||
.SetFilesPriorityAsync(hash, Arg.Is<List<int>>(idx => idx.Count == 2 && idx.Contains(1) && idx.Contains(2) && !idx.Contains(0)), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -545,8 +545,8 @@ public class DownloadCleanerIntegrationTests : IDisposable
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(
|
||||
List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) => [];
|
||||
|
||||
public override Task ChangeCategoryForNoHardLinksAsync(
|
||||
List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) => Task.CompletedTask;
|
||||
protected override Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(
|
||||
ITorrentItemWrapper torrent) => throw new NotSupportedException();
|
||||
|
||||
public override Task<List<ITorrentItemWrapper>> GetAllTorrentsLite() => Task.FromResult(_seedingDownloads);
|
||||
|
||||
@@ -564,7 +564,7 @@ public class DownloadCleanerIntegrationTests : IDisposable
|
||||
public override Task<DownloadCheckResult> ShouldRemoveFromArrQueueAsync(
|
||||
string hash, IReadOnlyList<string> ignoredDownloads) => throw new NotSupportedException();
|
||||
|
||||
public override Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag) =>
|
||||
protected override Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override Task CreateCategoryAsync(string name) => throw new NotSupportedException();
|
||||
|
||||
+22
-81
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
@@ -19,9 +17,9 @@ public partial class DelugeService
|
||||
DownloadStatus? download = await _client.GetTorrentStatus(hash);
|
||||
BlockFilesResult result = new();
|
||||
|
||||
if (download?.Hash is null)
|
||||
if (download?.Hash is null || download?.Name is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the {name} download client", hash, _downloadClientConfig.Name);
|
||||
_logger.LogDebug("failed to find torrent {Hash} in the {Name} download client", hash, _downloadClientConfig.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -32,7 +30,7 @@ public partial class DelugeService
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogInformation("skip | download is ignored | {name}", download.Name);
|
||||
_logger.LogInformation("skip | download is ignored | {Name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -41,7 +39,7 @@ public partial class DelugeService
|
||||
if (malwareBlockerConfig.IgnorePrivate && download.Private)
|
||||
{
|
||||
// ignore private trackers
|
||||
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
|
||||
_logger.LogDebug("skip files check | download is private | {Name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -53,94 +51,37 @@ public partial class DelugeService
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "failed to find files in the download client | {name}", download.Name);
|
||||
_logger.LogWarning(exception, "failed to find files in the download client | {Name}", download.Name);
|
||||
}
|
||||
|
||||
if (contents is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
Dictionary<int, int> priorities = [];
|
||||
bool hasPriorityUpdates = false;
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
|
||||
// Deluge's priority API takes the full per-file vector, so keep every original priority
|
||||
Dictionary<int, int> originalPriorities = [];
|
||||
List<(int Index, string ValidationName, string LogName, FileBlockAction Action)> scanItems = [];
|
||||
|
||||
ProcessFiles(contents.Contents, (name, file) =>
|
||||
{
|
||||
totalFiles++;
|
||||
int priority = file.Priority;
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.Priority is 0)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", file.Path);
|
||||
totalUnwantedFiles++;
|
||||
}
|
||||
|
||||
if (file.Priority is not 0 && !_filenameEvaluator.IsValid(name, blocklistType, patterns, regexes))
|
||||
{
|
||||
totalUnwantedFiles++;
|
||||
priority = 0;
|
||||
hasPriorityUpdates = true;
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogTrace("File is valid | {file}", file.Path);
|
||||
priorities.Add(file.Index, priority);
|
||||
originalPriorities[file.Index] = file.Priority;
|
||||
scanItems.Add((file.Index, name, file.Path, file.Priority is 0
|
||||
? FileBlockAction.AlreadySkipped
|
||||
: FileBlockAction.CheckBlocklist));
|
||||
});
|
||||
|
||||
if (result.ShouldRemove)
|
||||
await ApplyFileBlockingAsync(result, download.Name, scanItems, malwareBlockerConfig.DeleteIfAnyFileBlocked, async unwantedIndices =>
|
||||
{
|
||||
return result;
|
||||
}
|
||||
HashSet<int> unwantedLookup = [..unwantedIndices];
|
||||
List<int> sortedPriorities = originalPriorities
|
||||
.OrderBy(x => x.Key)
|
||||
.Select(x => unwantedLookup.Contains(x.Key) ? 0 : x.Value)
|
||||
.ToList();
|
||||
|
||||
if (!hasPriorityUpdates)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
_logger.LogDebug("changing priorities | torrent {hash}", hash);
|
||||
|
||||
List<int> sortedPriorities = priorities
|
||||
.OrderBy(x => x.Key)
|
||||
.Select(x => x.Value)
|
||||
.ToList();
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
return result;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Marking {count} unwanted files as skipped for {name}", totalUnwantedFiles, download.Name);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeFilesPriority(hash, sortedPriorities));
|
||||
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual async Task ChangeFilesPriority(string hash, List<int> sortedPriorities)
|
||||
{
|
||||
await _client.ChangeFilesPriority(hash, sortedPriorities);
|
||||
}
|
||||
}
|
||||
+27
-99
@@ -3,8 +3,6 @@ using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -67,17 +65,6 @@ public partial class DelugeService
|
||||
return relativePaths;
|
||||
});
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) =>
|
||||
downloads
|
||||
?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Where(x => unlinkedConfig.Categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task DeleteDownload(ITorrentItemWrapper torrent, bool deleteSourceFiles)
|
||||
{
|
||||
@@ -107,103 +94,44 @@ public partial class DelugeService
|
||||
await _dryRunInterceptor.InterceptAsync(() => CreateLabel(name));
|
||||
}
|
||||
|
||||
public override async Task ChangeCategoryForNoHardLinksAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
if (downloads?.Count is null or 0)
|
||||
DelugeItemWrapper delugeTorrent = (DelugeItemWrapper)torrent;
|
||||
DelugeContents? contents;
|
||||
|
||||
try
|
||||
{
|
||||
return;
|
||||
contents = await _client.GetTorrentFiles(delugeTorrent.Hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "failed to find torrent files for {Name}", delugeTorrent.Name);
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (DelugeItemWrapper torrent in downloads.Cast<DelugeItemWrapper>())
|
||||
List<(string FilePath, HardLinkScanAction Action)> scanItems = [];
|
||||
|
||||
ProcessFiles(contents?.Contents, (_, file) =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(torrent.Hash) || string.IsNullOrEmpty(torrent.Name) || string.IsNullOrEmpty(torrent.Category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(delugeTorrent.Info.DownloadLocation, file.Path),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
HardLinkScanAction action = file.Priority <= 0
|
||||
? HardLinkScanAction.SkipUnwanted
|
||||
: HardLinkScanAction.CheckHardLinks;
|
||||
|
||||
DelugeContents? contents;
|
||||
try
|
||||
{
|
||||
contents = await _client.GetTorrentFiles(torrent.Hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogWarning(exception, "failed to find torrent files for {name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
scanItems.Add((filePath, action));
|
||||
});
|
||||
|
||||
bool hasHardlinks = false;
|
||||
bool hasErrors = false;
|
||||
|
||||
ProcessFiles(contents?.Contents, (_, file) =>
|
||||
{
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(torrent.Info.DownloadLocation, file.Path),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
if (file.Priority <= 0)
|
||||
{
|
||||
_logger.LogDebug("skip | file is not downloaded | {file}", filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService
|
||||
.GetHardLinkCount(filePath, unlinkedConfig.IgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
_logger.LogError("skip | file does not exist or insufficient permissions | {file}", filePath);
|
||||
hasErrors = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hardlinkCount > 0)
|
||||
{
|
||||
hasHardlinks = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHardlinks)
|
||||
{
|
||||
_logger.LogDebug("skip | download has hardlinks | {name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabel(torrent.Hash, unlinkedConfig.TargetCategory));
|
||||
|
||||
_logger.LogInformation("category changed for {name}", torrent.Name);
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(torrent.Category, unlinkedConfig.TargetCategory);
|
||||
|
||||
torrent.Category = unlinkedConfig.TargetCategory;
|
||||
}
|
||||
return scanItems;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag)
|
||||
{
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabel(torrent.Hash, targetCategory));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, targetCategory);
|
||||
|
||||
torrent.Category = targetCategory;
|
||||
}
|
||||
protected override Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag) =>
|
||||
ChangeLabel(torrent.Hash, targetCategory);
|
||||
|
||||
protected async Task CreateLabel(string name)
|
||||
{
|
||||
|
||||
-43
@@ -2,9 +2,6 @@ using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.Deluge.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient.Deluge;
|
||||
@@ -76,44 +73,4 @@ public partial class DelugeService
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await CheckIfStuck(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, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(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, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.HealthCheck;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
@@ -72,6 +74,55 @@ public abstract class DownloadService : IDownloadService
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the slow check, then the stall check; the first removal wins.
|
||||
/// </summary>
|
||||
protected async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await CheckIfStuck(wrapper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alt-speed-limit probe for the slow rules.
|
||||
/// Only qBittorrent and Transmission report that state.
|
||||
/// </summary>
|
||||
protected virtual Func<Task<bool>>? GetAltSpeedLimitProbe() => null;
|
||||
|
||||
protected virtual 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, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {Name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper, GetAltSpeedLimitProbe());
|
||||
}
|
||||
|
||||
protected virtual 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, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract Task LoginAsync();
|
||||
@@ -176,10 +227,17 @@ public abstract class DownloadService : IDownloadService
|
||||
.TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules);
|
||||
public virtual List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig);
|
||||
public virtual List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) =>
|
||||
downloads
|
||||
?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Where(x => unlinkedConfig.Categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task CleanDownloadsAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules)
|
||||
@@ -272,10 +330,230 @@ public abstract class DownloadService : IDownloadService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task ChangeCategoryForNoHardLinksAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig);
|
||||
public async Task ChangeCategoryForNoHardLinksAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
|
||||
{
|
||||
if (downloads?.Count is null or 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ITorrentItemWrapper torrent in downloads)
|
||||
{
|
||||
if (string.IsNullOrEmpty(torrent.Hash) || string.IsNullOrEmpty(torrent.Name) || string.IsNullOrEmpty(torrent.Category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
IEnumerable<(string FilePath, HardLinkScanAction Action)>? files = await GetHardLinkScanItemsAsync(torrent);
|
||||
|
||||
if (files is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
(bool hasHardlinks, bool hasErrors) = ScanForHardLinks(files, unlinkedConfig.IgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHardlinks)
|
||||
{
|
||||
_logger.LogDebug("skip | download has hardlinks | {Name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
await ChangeTorrentCategoryAsync(torrent, unlinkedConfig.TargetCategory, unlinkedConfig.UseTag);
|
||||
|
||||
_logger.LogInformation(unlinkedConfig.UseTag && SupportsTags ? "tag added for {Name}" : "category changed for {Name}", torrent.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a torrent's files to (path, <see cref="HardLinkScanAction"/>) pairs.
|
||||
/// Returns null to skip the torrent, such as when its files can't be read.
|
||||
/// </summary>
|
||||
protected abstract Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(ITorrentItemWrapper torrent);
|
||||
|
||||
/// <summary>
|
||||
/// Stops at the first hardlink or unreadable file.
|
||||
/// Each client maps its files to (path, <see cref="HardLinkScanAction"/>) pairs.
|
||||
/// </summary>
|
||||
protected (bool HasHardlinks, bool HasErrors) ScanForHardLinks(
|
||||
IEnumerable<(string FilePath, HardLinkScanAction Action)> files,
|
||||
bool ignoreRootDirs)
|
||||
{
|
||||
foreach ((string filePath, HardLinkScanAction action) in files)
|
||||
{
|
||||
if (action is HardLinkScanAction.TreatAsLinked)
|
||||
{
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
if (action is HardLinkScanAction.SkipUnwanted)
|
||||
{
|
||||
_logger.LogDebug("skip | file is not downloaded | {File}", filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, ignoreRootDirs);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
_logger.LogError("skip | file does not exist or insufficient permissions | {File}", filePath);
|
||||
return (false, true);
|
||||
}
|
||||
|
||||
if (hardlinkCount > 0)
|
||||
{
|
||||
return (true, false);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts unwanted files and collects the indices to block.
|
||||
/// Don't yield a file the client excludes from the count, such as one with no index.
|
||||
/// Deluge and rTorrent validate the bare filename but log the relative path.
|
||||
/// With <paramref name="deleteIfAnyFileBlocked"/> on, the first unwanted file sets <c>DeleteImmediately</c> and ends the scan.
|
||||
/// </summary>
|
||||
protected (List<int> UnwantedIndices, long TotalFiles, long TotalUnwantedFiles, bool DeleteImmediately) ScanFilesForBlocking(
|
||||
IEnumerable<(int Index, string ValidationName, string LogName, FileBlockAction Action)> files,
|
||||
BlocklistType blocklistType,
|
||||
ConcurrentBag<string> patterns,
|
||||
ConcurrentBag<Regex> regexes,
|
||||
bool deleteIfAnyFileBlocked)
|
||||
{
|
||||
List<int> unwantedIndices = [];
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
foreach ((int index, string validationName, string logName, FileBlockAction action) in files)
|
||||
{
|
||||
totalFiles++;
|
||||
|
||||
if (action is FileBlockAction.AlreadySkipped)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {File}", logName);
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_filenameEvaluator.IsValid(validationName, blocklistType, patterns, regexes))
|
||||
{
|
||||
_logger.LogTrace("File is valid | {File}", logName);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {File}", logName);
|
||||
totalUnwantedFiles++;
|
||||
|
||||
if (deleteIfAnyFileBlocked)
|
||||
{
|
||||
return (unwantedIndices, totalFiles, totalUnwantedFiles, true);
|
||||
}
|
||||
|
||||
unwantedIndices.Add(index);
|
||||
}
|
||||
|
||||
return (unwantedIndices, totalFiles, totalUnwantedFiles, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans the files, sets the removal verdict on <paramref name="result"/> and marks the unwanted files as skipped.
|
||||
/// </summary>
|
||||
/// <param name="markFilesAsSkipped">Marks the given file indices as skipped in the client.</param>
|
||||
protected async Task ApplyFileBlockingAsync(
|
||||
BlockFilesResult result,
|
||||
string name,
|
||||
IEnumerable<(int Index, string ValidationName, string LogName, FileBlockAction Action)> files,
|
||||
bool deleteIfAnyFileBlocked,
|
||||
Func<List<int>, Task> markFilesAsSkipped)
|
||||
{
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
|
||||
(List<int> unwantedIndices, long totalFiles, long totalUnwantedFiles, bool deleteImmediately) =
|
||||
ScanFilesForBlocking(files, blocklistType, patterns, regexes, deleteIfAnyFileBlocked);
|
||||
|
||||
if (deleteImmediately)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {Name}", name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwantedIndices.Count is 0)
|
||||
{
|
||||
_logger.LogDebug("No unwanted files found for {Name}", name);
|
||||
return;
|
||||
}
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {Name}", name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Marking {Count} unwanted files as skipped for {Name}", unwantedIndices.Count, name);
|
||||
await _dryRunInterceptor.InterceptAsync(() => MarkFilesAsSkipped(name, unwantedIndices, markFilesAsSkipped));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A failed priority update leaves the verdict in place.
|
||||
/// </summary>
|
||||
private async Task MarkFilesAsSkipped(string name, List<int> unwantedIndices, Func<List<int>, Task> markFilesAsSkipped)
|
||||
{
|
||||
try
|
||||
{
|
||||
await markFilesAsSkipped(unwantedIndices);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Failed to mark files as skipped | {Name}", name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag);
|
||||
public async Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag)
|
||||
{
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
useTag = useTag && SupportsTags;
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeCategoryInClientAsync(torrent, targetCategory, useTag));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, targetCategory, useTag);
|
||||
|
||||
if (!useTag)
|
||||
{
|
||||
torrent.Category = targetCategory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clients without tags fall back to a category change when asked for a tag.
|
||||
/// </summary>
|
||||
protected virtual bool SupportsTags => false;
|
||||
|
||||
/// <summary>
|
||||
/// Applies the category, or the tag when <paramref name="useTag"/> is set, in the client.
|
||||
/// </summary>
|
||||
protected abstract Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task CreateCategoryAsync(string name);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
|
||||
/// <summary>
|
||||
/// What <see cref="DownloadService.ScanFilesForBlocking"/> does with one file.
|
||||
/// </summary>
|
||||
public enum FileBlockAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Already unwanted in the client; count it and skip the blocklist.
|
||||
/// </summary>
|
||||
AlreadySkipped,
|
||||
|
||||
/// <summary>
|
||||
/// Check the file name against the blocklist.
|
||||
/// </summary>
|
||||
CheckBlocklist
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
|
||||
/// <summary>
|
||||
/// What <see cref="DownloadService.ScanForHardLinks"/> does with one file.
|
||||
/// </summary>
|
||||
public enum HardLinkScanAction
|
||||
{
|
||||
/// <summary>
|
||||
/// File not downloaded; log and skip it.
|
||||
/// </summary>
|
||||
SkipUnwanted,
|
||||
|
||||
/// <summary>
|
||||
/// Read the file's hardlink count from disk.
|
||||
/// </summary>
|
||||
CheckHardLinks,
|
||||
|
||||
/// <summary>
|
||||
/// Count the torrent as hardlinked without touching disk.
|
||||
/// </summary>
|
||||
TreatAsLinked
|
||||
}
|
||||
+18
-67
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
@@ -73,77 +71,30 @@ public partial class QBitService
|
||||
return result;
|
||||
}
|
||||
|
||||
List<int> unwantedFiles = [];
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
|
||||
foreach (TorrentContent file in files)
|
||||
IEnumerable<(int Index, string ValidationName, string LogName, FileBlockAction Action)> BuildScanItems()
|
||||
{
|
||||
if (!file.Index.HasValue)
|
||||
foreach (TorrentContent file in files)
|
||||
{
|
||||
_logger.LogTrace("Skipping file with no index | {File}", file.Name);
|
||||
continue;
|
||||
if (!file.Index.HasValue)
|
||||
{
|
||||
_logger.LogTrace("Skipping file with no index | {File}", file.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return (file.Index.Value, file.Name, file.Name, file.Priority is TorrentContentPriority.Skip
|
||||
? FileBlockAction.AlreadySkipped
|
||||
: FileBlockAction.CheckBlocklist);
|
||||
}
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (file.Priority is TorrentContentPriority.Skip)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {File}", file.Name);
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_filenameEvaluator.IsValid(file.Name, blocklistType, patterns, regexes))
|
||||
{
|
||||
_logger.LogTrace("File is valid | {File}", file.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {File}", file.Name);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {Name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
|
||||
unwantedFiles.Add(file.Index.Value);
|
||||
totalUnwantedFiles++;
|
||||
}
|
||||
|
||||
if (unwantedFiles.Count is 0)
|
||||
await ApplyFileBlockingAsync(result, download.Name, BuildScanItems(), malwareBlockerConfig.DeleteIfAnyFileBlocked, async unwantedIndices =>
|
||||
{
|
||||
_logger.LogDebug("No unwanted files found for {Name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {Name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Marking {Count} unwanted files as skipped for {Name}", totalUnwantedFiles, download.Name);
|
||||
foreach (int fileIndex in unwantedIndices)
|
||||
{
|
||||
await _client.SetFilePriorityAsync(hash, fileIndex, TorrentContentPriority.Skip);
|
||||
}
|
||||
});
|
||||
|
||||
foreach (int fileIndex in unwantedFiles)
|
||||
{
|
||||
await _dryRunInterceptor.InterceptAsync(() => MarkFileAsSkipped(hash, fileIndex));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual async Task MarkFileAsSkipped(string hash, int fileIndex)
|
||||
{
|
||||
await _client.SetFilePriorityAsync(hash, fileIndex, TorrentContentPriority.Skip);
|
||||
}
|
||||
}
|
||||
+22
-89
@@ -1,6 +1,5 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -125,115 +124,49 @@ public partial class QBitService
|
||||
await _dryRunInterceptor.InterceptAsync(() => CreateCategory(name));
|
||||
}
|
||||
|
||||
public override async Task ChangeCategoryForNoHardLinksAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
if (downloads?.Count is null or 0)
|
||||
QBitItemWrapper qBitTorrent = (QBitItemWrapper)torrent;
|
||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(qBitTorrent.Hash);
|
||||
|
||||
if (files is null)
|
||||
{
|
||||
return;
|
||||
_logger.LogDebug("failed to find files for {Name}", qBitTorrent.Name);
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (QBitItemWrapper torrent in downloads.Cast<QBitItemWrapper>())
|
||||
IEnumerable<(string FilePath, HardLinkScanAction Action)> BuildScanItems()
|
||||
{
|
||||
if (string.IsNullOrEmpty(torrent.Name) || string.IsNullOrEmpty(torrent.Hash) || string.IsNullOrEmpty(torrent.Category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IReadOnlyList<TorrentContent>? files = await _client.GetTorrentContentsAsync(torrent.Hash);
|
||||
|
||||
if (files is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find files for {Name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
bool hasHardlinks = false;
|
||||
bool hasErrors = false;
|
||||
|
||||
foreach (TorrentContent file in files)
|
||||
{
|
||||
if (!file.Index.HasValue)
|
||||
{
|
||||
_logger.LogDebug("skip | file index is null for {Name}", torrent.Name);
|
||||
hasHardlinks = true;
|
||||
break;
|
||||
_logger.LogDebug("skip | file index is null for {Name}", qBitTorrent.Name);
|
||||
yield return (string.Empty, HardLinkScanAction.TreatAsLinked);
|
||||
yield break;
|
||||
}
|
||||
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(torrent.Info.SavePath, file.Name),
|
||||
Path.Combine(qBitTorrent.Info.SavePath, file.Name),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
if (file.Priority is TorrentContentPriority.Skip)
|
||||
{
|
||||
_logger.LogDebug("skip | file is not downloaded | {File}", filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, unlinkedConfig.IgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
_logger.LogError("skip | file does not exist or insufficient permissions | {File}", filePath);
|
||||
hasErrors = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hardlinkCount > 0)
|
||||
{
|
||||
hasHardlinks = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHardlinks)
|
||||
{
|
||||
_logger.LogDebug("skip | download has hardlinks | {Name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeCategory(torrent.Hash, unlinkedConfig.TargetCategory, unlinkedConfig.UseTag));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(torrent.Category, unlinkedConfig.TargetCategory, unlinkedConfig.UseTag);
|
||||
|
||||
if (unlinkedConfig.UseTag)
|
||||
{
|
||||
_logger.LogInformation("tag added for {Name}", torrent.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("category changed for {Name}", torrent.Name);
|
||||
torrent.Category = unlinkedConfig.TargetCategory;
|
||||
yield return (filePath, file.Priority is TorrentContentPriority.Skip
|
||||
? HardLinkScanAction.SkipUnwanted
|
||||
: HardLinkScanAction.CheckHardLinks);
|
||||
}
|
||||
}
|
||||
|
||||
return BuildScanItems();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag)
|
||||
{
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
protected override bool SupportsTags => true;
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeCategory(torrent.Hash, targetCategory, useTag));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, targetCategory, useTag);
|
||||
|
||||
if (!useTag)
|
||||
{
|
||||
torrent.Category = targetCategory;
|
||||
}
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
protected override async Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag) =>
|
||||
await ChangeCategory(torrent.Hash, targetCategory, useTag);
|
||||
|
||||
protected async Task CreateCategory(string name)
|
||||
{
|
||||
|
||||
+19
-47
@@ -83,62 +83,34 @@ public partial class QBitService
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
/// <inheritdoc/>
|
||||
protected override Func<Task<bool>>? GetAltSpeedLimitProbe() => IsAltSpeedLimitActiveAsync;
|
||||
|
||||
/// <summary>
|
||||
/// qBittorrent can stall on metadata before any content arrives.
|
||||
/// That stall gets its own strike before the shared stall check.
|
||||
/// </summary>
|
||||
protected override async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) slowResult = await CheckIfSlow(wrapper);
|
||||
|
||||
if (slowResult.ShouldRemove)
|
||||
if (!((QBitItemWrapper)wrapper).IsMetadataDownloading())
|
||||
{
|
||||
return slowResult;
|
||||
return await base.CheckIfStuck(wrapper);
|
||||
}
|
||||
|
||||
return await CheckIfStuck(wrapper);
|
||||
}
|
||||
QueueCleanerConfig queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>(nameof(QueueCleanerConfig));
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfSlow(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (!wrapper.IsDownloading())
|
||||
if (queueCleanerConfig.DownloadingMetadataMaxStrikes <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download is not in downloading state | {Name}", wrapper.Name);
|
||||
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, false);
|
||||
}
|
||||
bool shouldRemove = await _striker.StrikeAndCheckLimit(
|
||||
wrapper.Hash,
|
||||
wrapper.Name,
|
||||
queueCleanerConfig.DownloadingMetadataMaxStrikes,
|
||||
StrikeType.DownloadingMetadata
|
||||
);
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper, IsAltSpeedLimitActiveAsync);
|
||||
}
|
||||
|
||||
private async Task<(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory)> CheckIfStuck(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
if (((QBitItemWrapper)wrapper).IsMetadataDownloading())
|
||||
{
|
||||
var queueCleanerConfig = ContextProvider.Get<QueueCleanerConfig>(nameof(QueueCleanerConfig));
|
||||
|
||||
if (queueCleanerConfig.DownloadingMetadataMaxStrikes > 0)
|
||||
{
|
||||
bool shouldRemove = await _striker.StrikeAndCheckLimit(
|
||||
wrapper.Hash,
|
||||
wrapper.Name,
|
||||
queueCleanerConfig.DownloadingMetadataMaxStrikes,
|
||||
StrikeType.DownloadingMetadata
|
||||
);
|
||||
|
||||
return (shouldRemove, DeleteReason.DownloadingMetadata, shouldRemove, 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, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
return (shouldRemove, DeleteReason.DownloadingMetadata, shouldRemove, false);
|
||||
}
|
||||
}
|
||||
+14
-76
@@ -1,5 +1,3 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities.RTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
@@ -21,7 +19,7 @@ public partial class RTorrentService
|
||||
|
||||
if (download?.Hash is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the {name} download client", hash, _downloadClientConfig.Name);
|
||||
_logger.LogDebug("failed to find torrent {Hash} in the {Name} download client", hash, _downloadClientConfig.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -44,7 +42,7 @@ public partial class RTorrentService
|
||||
|
||||
if (malwareBlockerConfig.IgnorePrivate && download.IsPrivate == 1)
|
||||
{
|
||||
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
|
||||
_logger.LogDebug("skip files check | download is private | {Name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -56,7 +54,7 @@ public partial class RTorrentService
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to find files in the download client | {name}", download.Name);
|
||||
_logger.LogDebug(exception, "failed to find files in the download client | {Name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -65,84 +63,24 @@ public partial class RTorrentService
|
||||
return result;
|
||||
}
|
||||
|
||||
bool hasPriorityUpdates = false;
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
|
||||
List<(int Index, int Priority)> priorityUpdates = [];
|
||||
|
||||
foreach (var file in files)
|
||||
IEnumerable<(int Index, string ValidationName, string LogName, FileBlockAction Action)> BuildScanItems()
|
||||
{
|
||||
totalFiles++;
|
||||
string fileName = Path.GetFileName(file.Path);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
foreach (RTorrentFile file in files)
|
||||
{
|
||||
continue;
|
||||
yield return (file.Index, Path.GetFileName(file.Path), file.Path, file.Priority == 0
|
||||
? FileBlockAction.AlreadySkipped
|
||||
: FileBlockAction.CheckBlocklist);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.Priority == 0)
|
||||
await ApplyFileBlockingAsync(result, download.Name, BuildScanItems(), malwareBlockerConfig.DeleteIfAnyFileBlocked, async unwantedIndices =>
|
||||
{
|
||||
foreach (int index in unwantedIndices)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", file.Path);
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
await _client.SetFilePriorityAsync(hash, index, 0);
|
||||
}
|
||||
|
||||
if (!_filenameEvaluator.IsValid(fileName, blocklistType, patterns, regexes))
|
||||
{
|
||||
totalUnwantedFiles++;
|
||||
hasPriorityUpdates = true;
|
||||
priorityUpdates.Add((file.Index, 0));
|
||||
_logger.LogInformation("unwanted file found | {file}", file.Path);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogTrace("File is valid | {file}", file.Path);
|
||||
}
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!hasPriorityUpdates)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Marking {count} unwanted files as skipped for {name}", priorityUpdates.Count, download.Name);
|
||||
|
||||
foreach (var (index, priority) in priorityUpdates)
|
||||
{
|
||||
await _dryRunInterceptor.InterceptAsync(() => SetFilePriority(hash, index, priority));
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual async Task SetFilePriority(string hash, int index, int priority)
|
||||
{
|
||||
await _client.SetFilePriorityAsync(hash, index, priority);
|
||||
}
|
||||
}
|
||||
+26
-101
@@ -1,7 +1,5 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.RTorrent.Response;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -66,17 +64,6 @@ public partial class RTorrentService
|
||||
return Task.FromResult<IReadOnlyList<string>>(claimed.ToList());
|
||||
}
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) =>
|
||||
downloads
|
||||
?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Where(x => unlinkedConfig.Categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task DeleteDownload(ITorrentItemWrapper torrent, bool deleteSourceFiles)
|
||||
{
|
||||
@@ -113,104 +100,42 @@ public partial class RTorrentService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task ChangeCategoryForNoHardLinksAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
if (downloads?.Count is null or 0)
|
||||
RTorrentItemWrapper rTorrent = (RTorrentItemWrapper)torrent;
|
||||
List<RTorrentFile> files;
|
||||
|
||||
try
|
||||
{
|
||||
return;
|
||||
files = await _client.GetTorrentFilesAsync(rTorrent.Hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to find torrent files for {Name}", rTorrent.Name);
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (RTorrentItemWrapper torrent in downloads.Cast<RTorrentItemWrapper>())
|
||||
List<(string FilePath, HardLinkScanAction Action)> scanItems = [];
|
||||
|
||||
foreach (RTorrentFile file in files)
|
||||
{
|
||||
if (string.IsNullOrEmpty(torrent.Hash) || string.IsNullOrEmpty(torrent.Name) || string.IsNullOrEmpty(torrent.Category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(rTorrent.Info.Directory ?? rTorrent.Info.BasePath ?? "", file.Path),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
List<RTorrentFile> files;
|
||||
try
|
||||
{
|
||||
files = await _client.GetTorrentFilesAsync(torrent.Hash);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogDebug(exception, "failed to find torrent files for {name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasHardlinks = false;
|
||||
bool hasErrors = false;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(torrent.Info.Directory ?? torrent.Info.BasePath ?? "", file.Path),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
if (file.Priority <= 0)
|
||||
{
|
||||
_logger.LogDebug("skip | file is not downloaded | {file}", filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService
|
||||
.GetHardLinkCount(filePath, unlinkedConfig.IgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
_logger.LogError("skip | file does not exist or insufficient permissions | {file}", filePath);
|
||||
hasErrors = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hardlinkCount > 0)
|
||||
{
|
||||
hasHardlinks = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHardlinks)
|
||||
{
|
||||
_logger.LogDebug("skip | download has hardlinks | {name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabel(torrent.Hash, unlinkedConfig.TargetCategory));
|
||||
|
||||
_logger.LogInformation("category changed for {name}", torrent.Name);
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(torrent.Category, unlinkedConfig.TargetCategory);
|
||||
|
||||
torrent.Category = unlinkedConfig.TargetCategory;
|
||||
scanItems.Add((filePath, file.Priority <= 0
|
||||
? HardLinkScanAction.SkipUnwanted
|
||||
: HardLinkScanAction.CheckHardLinks));
|
||||
}
|
||||
|
||||
return scanItems;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag)
|
||||
{
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabel(torrent.Hash, targetCategory));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, targetCategory);
|
||||
|
||||
torrent.Category = targetCategory;
|
||||
}
|
||||
protected override Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag) =>
|
||||
ChangeLabel(torrent.Hash, targetCategory);
|
||||
|
||||
protected virtual async Task ChangeLabel(string hash, string newLabel)
|
||||
{
|
||||
|
||||
-40
@@ -67,44 +67,4 @@ public partial class RTorrentService
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await CheckIfStuck(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, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(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, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
+25
-64
@@ -1,10 +1,9 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
@@ -17,21 +16,21 @@ public partial class TransmissionService
|
||||
TorrentInfo? download = await GetTorrentAsync(hash);
|
||||
BlockFilesResult result = new();
|
||||
|
||||
if (download?.FileStats is null || download.FileStats.Length == 0)
|
||||
if (download?.FileStats is null || download.FileStats.Length == 0 || download.Name is null)
|
||||
{
|
||||
_logger.LogDebug("failed to find torrent {hash} in the {name} download client", hash, _downloadClientConfig.Name);
|
||||
_logger.LogDebug("Failed to find torrent {Hash} in the {Name} download client", hash, _downloadClientConfig.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (download.Files is null)
|
||||
{
|
||||
_logger.LogDebug("torrent {hash} has no files", hash);
|
||||
_logger.LogDebug("Torrent {Hash} has no files", hash);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (ignoredDownloads.Count > 0 && download.ShouldIgnore(ignoredDownloads))
|
||||
{
|
||||
_logger.LogDebug("skip | download is ignored | {name}", download.Name);
|
||||
_logger.LogDebug("skip | download is ignored | {Name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -46,72 +45,34 @@ public partial class TransmissionService
|
||||
if (malwareBlockerConfig.IgnorePrivate && isPrivate)
|
||||
{
|
||||
// ignore private trackers
|
||||
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
|
||||
_logger.LogDebug("skip files check | download is private | {Name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
List<long> unwantedFiles = [];
|
||||
long totalFiles = 0;
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
|
||||
for (int i = 0; i < download.Files.Length; i++)
|
||||
IEnumerable<(int Index, string ValidationName, string LogName, FileBlockAction Action)> BuildScanItems()
|
||||
{
|
||||
if (download.FileStats?[i].Wanted == null)
|
||||
for (int i = 0; i < download.Files.Length; i++)
|
||||
{
|
||||
_logger.LogTrace("Skipping file with no stats | {file}", download.Files[i].Name);
|
||||
continue;
|
||||
if (download.FileStats?[i].Wanted == null)
|
||||
{
|
||||
_logger.LogTrace("Skipping file with no stats | {File}", download.Files[i].Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return (i, download.Files[i].Name, download.Files[i].Name, download.FileStats[i].Wanted!.Value
|
||||
? FileBlockAction.CheckBlocklist
|
||||
: FileBlockAction.AlreadySkipped);
|
||||
}
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (!download.FileStats[i].Wanted.Value)
|
||||
{
|
||||
_logger.LogTrace("File is already skipped | {file}", download.Files[i].Name);
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_filenameEvaluator.IsValid(download.Files[i].Name, blocklistType, patterns, regexes))
|
||||
{
|
||||
_logger.LogTrace("File is valid | {file}", download.Files[i].Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("unwanted file found | {file}", download.Files[i].Name);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
|
||||
unwantedFiles.Add(i);
|
||||
totalUnwantedFiles++;
|
||||
}
|
||||
|
||||
if (unwantedFiles.Count is 0)
|
||||
await ApplyFileBlockingAsync(result, download.Name, BuildScanItems(), malwareBlockerConfig.DeleteIfAnyFileBlocked, async unwantedIndices =>
|
||||
{
|
||||
_logger.LogDebug("No unwanted files found for {name}", download.Name);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (totalUnwantedFiles == totalFiles)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Marking {count} unwanted files as skipped for {name}", totalUnwantedFiles, download.Name);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => SetUnwantedFiles(download.Id, unwantedFiles.ToArray()));
|
||||
await _client.TorrentSetAsync(new TorrentSettings
|
||||
{
|
||||
Ids = [download.Id],
|
||||
FilesUnwanted = unwantedIndices.Select(i => (long)i).ToArray(),
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+28
-104
@@ -1,6 +1,5 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -54,13 +53,6 @@ public partial class TransmissionService
|
||||
});
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules)
|
||||
{
|
||||
return downloads
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
|
||||
{
|
||||
return downloads
|
||||
@@ -98,37 +90,28 @@ public partial class TransmissionService
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task ChangeCategoryForNoHardLinksAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
|
||||
/// <inheritdoc/>
|
||||
protected override Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
if (downloads?.Count is null or 0)
|
||||
TransmissionItemWrapper transmissionTorrent = (TransmissionItemWrapper)torrent;
|
||||
|
||||
if (string.IsNullOrEmpty(transmissionTorrent.Info.DownloadDir))
|
||||
{
|
||||
return;
|
||||
return Task.FromResult<IEnumerable<(string FilePath, HardLinkScanAction Action)>?>(null);
|
||||
}
|
||||
|
||||
foreach (TransmissionItemWrapper torrent in downloads.Cast<TransmissionItemWrapper>())
|
||||
if (transmissionTorrent.Info.Files is null || transmissionTorrent.Info.FileStats is null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(torrent.Hash) || string.IsNullOrEmpty(torrent.Name) || string.IsNullOrEmpty(torrent.Info.DownloadDir))
|
||||
_logger.LogDebug("skip | download has no files | {name}", transmissionTorrent.Name);
|
||||
return Task.FromResult<IEnumerable<(string FilePath, HardLinkScanAction Action)>?>(null);
|
||||
}
|
||||
|
||||
IEnumerable<(string FilePath, HardLinkScanAction Action)> BuildScanItems()
|
||||
{
|
||||
for (int i = 0; i < transmissionTorrent.Info.Files.Length; i++)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
if (torrent.Info.Files is null || torrent.Info.FileStats is null)
|
||||
{
|
||||
_logger.LogDebug("skip | download has no files | {name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasHardlinks = false;
|
||||
bool hasErrors = false;
|
||||
|
||||
for (int i = 0; i < torrent.Info.Files.Length; i++)
|
||||
{
|
||||
TransmissionTorrentFiles file = torrent.Info.Files[i];
|
||||
TransmissionTorrentFileStats stats = torrent.Info.FileStats[i];
|
||||
TransmissionTorrentFiles file = transmissionTorrent.Info.Files[i];
|
||||
TransmissionTorrentFileStats stats = transmissionTorrent.Info.FileStats[i];
|
||||
|
||||
if (stats.Wanted is null or false || string.IsNullOrEmpty(file.Name))
|
||||
{
|
||||
@@ -136,99 +119,40 @@ public partial class TransmissionService
|
||||
}
|
||||
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(torrent.Info.DownloadDir, file.Name),
|
||||
Path.Combine(transmissionTorrent.Info.DownloadDir, file.Name),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, unlinkedConfig.IgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
_logger.LogError("skip | file does not exist or insufficient permissions | {file}", filePath);
|
||||
hasErrors = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hardlinkCount > 0)
|
||||
{
|
||||
hasHardlinks = true;
|
||||
break;
|
||||
}
|
||||
yield return (filePath, HardLinkScanAction.CheckHardLinks);
|
||||
}
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHardlinks)
|
||||
{
|
||||
_logger.LogDebug("skip | download has hardlinks | {name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
|
||||
if (unlinkedConfig.UseTag)
|
||||
{
|
||||
string[] newLabels = torrent.Tags
|
||||
.Append(unlinkedConfig.TargetCategory)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabels(torrent.Info.Id, newLabels));
|
||||
|
||||
_logger.LogInformation("label added for {name}", torrent.Name);
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, unlinkedConfig.TargetCategory, isTag: true);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
string newLocation = torrent.Info.GetNewLocationByAppend(unlinkedConfig.TargetCategory);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeDownloadLocation(torrent.Info.Id, newLocation));
|
||||
|
||||
_logger.LogInformation("category changed for {name}", torrent.Name);
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, unlinkedConfig.TargetCategory);
|
||||
|
||||
torrent.Category = unlinkedConfig.TargetCategory;
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<(string FilePath, HardLinkScanAction Action)>?>(BuildScanItems());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag)
|
||||
protected override bool SupportsTags => true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag)
|
||||
{
|
||||
var transmissionTorrent = (TransmissionItemWrapper)torrent;
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
TransmissionItemWrapper transmissionTorrent = (TransmissionItemWrapper)torrent;
|
||||
|
||||
if (useTag)
|
||||
{
|
||||
string[] newLabels = torrent.Tags
|
||||
string[] newLabels = transmissionTorrent.Tags
|
||||
.Append(targetCategory)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabels(transmissionTorrent.Info.Id, newLabels));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, targetCategory, isTag: true);
|
||||
await ChangeLabels(transmissionTorrent.Info.Id, newLabels);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string newLocation = transmissionTorrent.Info.GetNewLocationByAppend(targetCategory);
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeDownloadLocation(transmissionTorrent.Info.Id, newLocation));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, targetCategory);
|
||||
|
||||
torrent.Category = targetCategory;
|
||||
await ChangeDownloadLocation(transmissionTorrent.Info.Id, newLocation);
|
||||
}
|
||||
|
||||
protected virtual async Task ChangeDownloadLocation(long downloadId, string newLocation)
|
||||
|
||||
+3
-56
@@ -1,11 +1,5 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Transmission.API.RPC.Arguments;
|
||||
using Transmission.API.RPC.Entity;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
@@ -72,53 +66,6 @@ public partial class TransmissionService
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual async Task SetUnwantedFiles(long downloadId, long[] unwantedFiles)
|
||||
{
|
||||
await _client.TorrentSetAsync(new TorrentSettings
|
||||
{
|
||||
Ids = [downloadId],
|
||||
FilesUnwanted = unwantedFiles,
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await CheckIfStuck(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, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(wrapper, IsAltSpeedLimitActiveAsync);
|
||||
}
|
||||
|
||||
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, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
protected override Func<Task<bool>>? GetAltSpeedLimitProbe() => IsAltSpeedLimitActiveAsync;
|
||||
}
|
||||
+10
-51
@@ -1,5 +1,3 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Extensions;
|
||||
@@ -63,61 +61,22 @@ public partial class UTorrentService
|
||||
return result;
|
||||
}
|
||||
|
||||
List<int> fileIndexes = new(files.Count);
|
||||
long totalUnwantedFiles = 0;
|
||||
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
IEnumerable<(int Index, string ValidationName, string LogName, FileBlockAction Action)> BuildScanItems()
|
||||
{
|
||||
var file = files[i];
|
||||
|
||||
if (file.Priority == 0) // Already skipped
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
{
|
||||
totalUnwantedFiles++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.Priority != 0 && !_filenameEvaluator.IsValid(file.Name, blocklistType, patterns, regexes))
|
||||
{
|
||||
totalUnwantedFiles++;
|
||||
fileIndexes.Add(i);
|
||||
_logger.LogInformation("unwanted file found | {File}", file.Name);
|
||||
|
||||
if (malwareBlockerConfig.DeleteIfAnyFileBlocked)
|
||||
{
|
||||
_logger.LogDebug("at least one file is blocked for {Name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AtLeastOneFileBlocked;
|
||||
return result;
|
||||
}
|
||||
UTorrentFile file = files[i];
|
||||
yield return (i, file.Name, file.Name, file.Priority == 0
|
||||
? FileBlockAction.AlreadySkipped
|
||||
: FileBlockAction.CheckBlocklist);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileIndexes.Count is 0)
|
||||
await ApplyFileBlockingAsync(result, download.Name, BuildScanItems(), malwareBlockerConfig.DeleteIfAnyFileBlocked, async unwantedIndices =>
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
_logger.LogDebug("changing priorities | torrent {Hash}", hash);
|
||||
|
||||
if (totalUnwantedFiles == files.Count)
|
||||
{
|
||||
_logger.LogDebug("All files are blocked for {Name}", download.Name);
|
||||
result.ShouldRemove = true;
|
||||
result.DeleteReason = DeleteReason.AllFilesBlocked;
|
||||
}
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeFilesPriority(hash, fileIndexes));
|
||||
await _client.SetFilesPriorityAsync(hash, unwantedIndices, 0);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual async Task ChangeFilesPriority(string hash, List<int> fileIndexes)
|
||||
{
|
||||
await _client.SetFilesPriorityAsync(hash, fileIndexes, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-95
@@ -1,9 +1,7 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent.Extensions;
|
||||
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
|
||||
using Cleanuparr.Shared.Helpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -60,17 +58,6 @@ public partial class UTorrentService
|
||||
return files?.Select(f => f.Name).Where(name => !string.IsNullOrEmpty(name)).ToList() ?? [];
|
||||
});
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<ISeedingRule> seedingRules) =>
|
||||
downloads
|
||||
?.Where(x => seedingRules.Any(rule => rule.Categories.Any(cat => cat.Equals(x.Category, StringComparison.OrdinalIgnoreCase))))
|
||||
.ToList();
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig) =>
|
||||
downloads
|
||||
?.Where(x => !string.IsNullOrEmpty(x.Hash))
|
||||
.Where(x => unlinkedConfig.Categories.Any(cat => cat.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task DeleteDownload(ITorrentItemWrapper torrent, bool deleteSourceFiles)
|
||||
{
|
||||
@@ -89,95 +76,32 @@ public partial class UTorrentService
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task ChangeCategoryForNoHardLinksAsync(List<ITorrentItemWrapper>? downloads, UnlinkedConfig unlinkedConfig)
|
||||
/// <inheritdoc/>
|
||||
protected override async Task<IEnumerable<(string FilePath, HardLinkScanAction Action)>?> GetHardLinkScanItemsAsync(ITorrentItemWrapper torrent)
|
||||
{
|
||||
if (downloads?.Count is null or 0)
|
||||
UTorrentItemWrapper uTorrent = (UTorrentItemWrapper)torrent;
|
||||
List<UTorrentFile>? files = await _client.GetTorrentFilesAsync(uTorrent.Hash);
|
||||
|
||||
List<(string FilePath, HardLinkScanAction Action)> scanItems = [];
|
||||
|
||||
foreach (UTorrentFile file in files ?? [])
|
||||
{
|
||||
return;
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(uTorrent.Info.SavePath, file.Name),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
scanItems.Add((filePath, file.Priority <= 0
|
||||
? HardLinkScanAction.SkipUnwanted
|
||||
: HardLinkScanAction.CheckHardLinks));
|
||||
}
|
||||
|
||||
foreach (UTorrentItemWrapper torrent in downloads.Cast<UTorrentItemWrapper>())
|
||||
{
|
||||
if (string.IsNullOrEmpty(torrent.Hash) || string.IsNullOrEmpty(torrent.Name) || string.IsNullOrEmpty(torrent.Category))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
List<UTorrentFile>? files = await _client.GetTorrentFilesAsync(torrent.Hash);
|
||||
|
||||
bool hasHardlinks = false;
|
||||
bool hasErrors = false;
|
||||
|
||||
foreach (var file in files ?? [])
|
||||
{
|
||||
string filePath = PathHelper.NormalizeAndRemap(
|
||||
Path.Combine(torrent.Info.SavePath, file.Name),
|
||||
_downloadClientConfig.DownloadDirectorySource,
|
||||
_downloadClientConfig.DownloadDirectoryTarget);
|
||||
|
||||
if (file.Priority <= 0)
|
||||
{
|
||||
_logger.LogDebug("skip | file is not downloaded | {File}", filePath);
|
||||
continue;
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService
|
||||
.GetHardLinkCount(filePath, unlinkedConfig.IgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
_logger.LogError("skip | file does not exist or insufficient permissions | {File}", filePath);
|
||||
hasErrors = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hardlinkCount > 0)
|
||||
{
|
||||
hasHardlinks = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasErrors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasHardlinks)
|
||||
{
|
||||
_logger.LogDebug("skip | download has hardlinks | {Name}", torrent.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabel(torrent.Hash, unlinkedConfig.TargetCategory));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(torrent.Category, unlinkedConfig.TargetCategory);
|
||||
|
||||
_logger.LogInformation("category changed for {Name}", torrent.Name);
|
||||
|
||||
torrent.Category = unlinkedConfig.TargetCategory;
|
||||
}
|
||||
return scanItems;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ChangeTorrentCategoryAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag)
|
||||
{
|
||||
ContextProvider.Set(ContextProvider.Keys.ItemName, torrent.Name);
|
||||
ContextProvider.Set(ContextProvider.Keys.Hash, torrent.Hash);
|
||||
SetDownloadClientContext();
|
||||
|
||||
string currentCategory = torrent.Category ?? string.Empty;
|
||||
|
||||
await _dryRunInterceptor.InterceptAsync(() => ChangeLabel(torrent.Hash, targetCategory));
|
||||
|
||||
await _eventPublisher.PublishCategoryChanged(currentCategory, targetCategory);
|
||||
|
||||
torrent.Category = targetCategory;
|
||||
}
|
||||
protected override Task ChangeCategoryInClientAsync(ITorrentItemWrapper torrent, string targetCategory, bool useTag) =>
|
||||
ChangeLabel(torrent.Hash, targetCategory);
|
||||
|
||||
protected virtual async Task ChangeLabel(string hash, string newLabel)
|
||||
{
|
||||
|
||||
-44
@@ -1,10 +1,7 @@
|
||||
using Cleanuparr.Domain.Entities;
|
||||
using Cleanuparr.Domain.Entities.UTorrent.Response;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Context;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent.Extensions;
|
||||
using Cleanuparr.Infrastructure.Services.Interfaces;
|
||||
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
|
||||
@@ -82,45 +79,4 @@ public partial class UTorrentService
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<(bool, DeleteReason, bool, bool)> EvaluateDownloadRemoval(ITorrentItemWrapper wrapper)
|
||||
{
|
||||
(bool ShouldRemove, DeleteReason Reason, bool DeleteFromClient, bool ChangeCategory) result = await CheckIfSlow(wrapper);
|
||||
|
||||
if (result.ShouldRemove)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return await CheckIfStuck(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, false);
|
||||
}
|
||||
|
||||
if (wrapper.DownloadSpeed <= 0)
|
||||
{
|
||||
_logger.LogTrace("skip slow check | download speed is 0 | {Name}", wrapper.Name);
|
||||
return (false, DeleteReason.None, false, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateSlowRulesAsync(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, false);
|
||||
}
|
||||
|
||||
return await _queueRuleEvaluator.EvaluateStallRulesAsync(wrapper);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user