diff --git a/server/RdtClient.Service.Test/Services/DownloadableFileFilterTest.cs b/server/RdtClient.Service.Test/Services/DownloadableFileFilterTest.cs new file mode 100644 index 0000000..3858ee2 --- /dev/null +++ b/server/RdtClient.Service.Test/Services/DownloadableFileFilterTest.cs @@ -0,0 +1,261 @@ +using Microsoft.Extensions.Logging; +using Moq; +using RdtClient.Data.Models.Data; +using RdtClient.Service.Services; + +namespace RdtClient.Service.Test.Services; + +public class DownloadableFileFilterTest +{ + [Fact] + public void IsDownloadable_WhenNoFilterSpecified_ReturnsTrue() + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1" + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, "file.txt", 10000); + + // Assert + Assert.True(result); + } + + [Theory] + // downloadMinSize is in MB, fileSize is in B + [InlineData(100, 20 * 1024 * 1024)] + [InlineData(2, 2 * 1024 * 1024)] + [InlineData(2, 2 * (1000 * 1000 + 1))] // mostly to show we use 1024 not 1000 for conversion + public void IsDownloadable_WhenDownloadMinSizeSpecified_AndDownloadBelowSize_ReturnsFalse(Int32 downloadMinSize, Int64 fileSize) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + DownloadMinSize = downloadMinSize + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, "file.txt", fileSize); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(100, 110 * 1024 * 1024)] + [InlineData(2, 2 * 1024 * 1024 + 1)] + public void IsDownloadable_WhenDownloadMinSizeSpecified_AndDownloadAboveSize_ReturnsTrue(Int32 downloadMinSize, Int64 fileSize) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + DownloadMinSize = downloadMinSize + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, "file.txt", fileSize); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("file", "no-match")] + [InlineData("file", "even/in/a/subdirectory.txt")] + [InlineData("ch[aA]racter c[lL]asses", "nope.txt")] + [InlineData("digits\\d+", "123 not matching.txt")] + public void IsDownloadable_WhenIncludeRegexSpecified_AndPathDoesNotMatchRegex_ReturnsFalse(String includeRegex, String filePath) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + IncludeRegex = includeRegex + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, filePath, Int64.MaxValue); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("file", "file.txt")] + [InlineData("file", "file/in/a/subdirectory.txt")] + [InlineData("ch[aA]racter c[lL]asses", "character cLasses")] + [InlineData("digits\\d+", "digits123456.txt")] + public void IsDownloadable_WhenIncludeRegexSpecified_AndPathMatchesRegex_ReturnsTrue(String includeRegex, String filePath) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + IncludeRegex = includeRegex + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, filePath, Int64.MaxValue); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("file", "no-match")] + [InlineData("file", "even/in/a/subdirectory.txt")] + [InlineData("ch[aA]racter c[lL]asses", "nope.txt")] + [InlineData("digits\\d+", "123 not matching.txt")] + public void IsDownloadable_WhenExcludeRegexSpecified_AndPathDoesNotMatchRegex_ReturnsTrue(String excludeRegex, String filePath) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + ExcludeRegex = excludeRegex + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, filePath, Int64.MaxValue); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("file", "file.txt")] + [InlineData("file", "file/in/a/subdirectory.txt")] + [InlineData("ch[aA]racter c[lL]asses", "character cLasses")] + [InlineData("digits\\d+", "digits123456.txt")] + public void IsDownloadable_WhenExcludeRegexSpecified_AndPathMatchesRegex_ReturnsFalse(String excludeRegex, String filePath) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + ExcludeRegex = excludeRegex + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, filePath, Int64.MaxValue); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("file", "file", "file.txt")] + [InlineData("file", "in/a", "file/in/a/subdirectory.txt")] + [InlineData("ch[aA]racter c[lL]asses", "character", "character cLasses")] + [InlineData("digits\\d+", "123456", "digits123456.txt")] + public void IsDownloadable_WhenBothIncludeAndExcludeRegexSpecified_AndPathMatchesIncludeAndExcludeRegex_ReturnsTrue(String includeRegex, String excludeRegex, String filePath) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + IncludeRegex = includeRegex, + ExcludeRegex = excludeRegex + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, filePath, Int64.MaxValue); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData(10, "file", 10 * 1024 * 1024 + 1, "no-match.txt")] + public void IsDownloadable_WhenBothDownloadMinSizeAndIncludeRegexSpecified_AndDownloadAboveSizeAndDoesNotMatchRegex_ReturnsFalse( + Int32 minSize, + String includeRegex, + Int64 fileSize, + String filePath) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + IncludeRegex = includeRegex, + DownloadMinSize = minSize + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, filePath, fileSize); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData(10, "file", 10 * 1024 * 1024 - 1, "file.txt")] + public void IsDownloadable_WhenBothDownloadMinSizeAndIncludeRegexSpecified_AndDownloadBelowSizeAndMatchesRegex_ReturnsFalse( + Int32 minSize, + String includeRegex, + Int64 fileSize, + String filePath) + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1", + IncludeRegex = includeRegex, + DownloadMinSize = minSize + }; + + var fileFilter = new DownloadableFileFilter(mocks.LoggerMock.Object); + + // Act + var result = fileFilter.IsDownloadable(torrent, filePath, fileSize); + + // Assert + Assert.False(result); + } + private class Mocks + { + public readonly Mock> LoggerMock = new(); + } +} \ No newline at end of file diff --git a/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs b/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs index 3c23861..29d740f 100644 --- a/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs +++ b/server/RdtClient.Service.Test/Services/TorrentClients/AllDebridTorrentClientTest.cs @@ -4,6 +4,7 @@ using Moq; using Newtonsoft.Json; using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; +using RdtClient.Service.Services; using RdtClient.Service.Services.TorrentClients; using File = AllDebridNET.File; @@ -69,60 +70,6 @@ public class AllDebridTorrentClientTest CompletionDate = new DateTimeOffset(2020, 1, 1, 1, 5, 0, TimeSpan.Zero).Second }; - private static readonly List IncludeExcludeFiles = - [ - new() - { - FolderOrFileName = "include.txt", - Size = 1, - DownloadLink = "https://fake.url/include.txt" - }, - new() - { - FolderOrFileName = "exclude.txt", - Size = 1, - DownloadLink = "https://fake.url/exclude.txt" - }, - new() - { - FolderOrFileName = "include-folder", - SubNodes = - [ - new File - { - FolderOrFileName = "include-folder-exclude-file.txt", - Size = 1, - DownloadLink = "https://fake.url/include-folder-exclude-file.txt" - }, - new File - { - FolderOrFileName = "include-folder-include-file.txt", - Size = 1, - DownloadLink = "https://fake.url/include-folder-include-file.txt" - } - ] - }, - new() - { - FolderOrFileName = "exclude-folder", - SubNodes = - [ - new File - { - FolderOrFileName = "exclude-folder-exclude-file.txt", - Size = 1, - DownloadLink = "https://fake.url/exclude-folder-exclude-file.txt" - }, - new File - { - FolderOrFileName = "exclude-folder-include-file.txt", - Size = 1, - DownloadLink = "https://fake.url/exclude-folder-include-file.txt" - } - ] - } - ]; - [Fact] public async Task GetTorrents_WhenFullSyncNoTorrents_ReturnsEmptyList() { @@ -137,7 +84,7 @@ public class AllDebridTorrentClientTest Fullsync = true }); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act var result = await allDebridTorrentClient.GetTorrents(); @@ -167,7 +114,7 @@ public class AllDebridTorrentClientTest Fullsync = false }); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act var result = await allDebridTorrentClient.GetTorrents(); @@ -197,7 +144,7 @@ public class AllDebridTorrentClientTest Fullsync = true }); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act @@ -233,7 +180,7 @@ public class AllDebridTorrentClientTest Fullsync = false }); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act @@ -275,7 +222,7 @@ public class AllDebridTorrentClientTest Fullsync = false }); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act @@ -322,7 +269,7 @@ public class AllDebridTorrentClientTest Fullsync = true }); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act @@ -347,7 +294,7 @@ public class AllDebridTorrentClientTest }; var serializedOriginal = JsonConvert.SerializeObject(torrent); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act var result = await allDebridTorrentClient.UpdateData(torrent, null); @@ -373,7 +320,7 @@ public class AllDebridTorrentClientTest }; mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny())).ReturnsAsync(Magnet1Finished); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act var result = await allDebridTorrentClient.UpdateData(torrent, null); @@ -406,7 +353,7 @@ public class AllDebridTorrentClientTest mocks.AllDebridClientMock.Setup(c => c.Magnet.StatusAsync(torrent.RdId, It.IsAny())) .ThrowsAsync(new AllDebridException("Magnet not found", "MAGNET_INVALID_ID")); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act var result = await allDebridTorrentClient.UpdateData(torrent, null); @@ -441,7 +388,7 @@ public class AllDebridTorrentClientTest Fullsync = true }); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); var torrentClientTorrent = (await allDebridTorrentClient.GetTorrents()).First(); // Act @@ -468,7 +415,7 @@ public class AllDebridTorrentClientTest RdId = null }; - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act var result = await allDebridTorrentClient.GetDownloadLinks(torrent); @@ -479,7 +426,57 @@ public class AllDebridTorrentClientTest } [Fact] - public async Task GetDownloadLinks_ByDefault_GetsLinksForAllFiles() + public async Task GetDownloadLinks_UsesFileFilter() + { + // Arrange + var mocks = new Mocks(); + + var torrent = new Torrent + { + RdId = "1" + }; + + List files = + [ + new() + { + FolderOrFileName = "file1.txt", + Size = 123, + DownloadLink = "https://fake.url/file1.txt" + }, + + new() + { + FolderOrFileName = "folder", + SubNodes = + [ + new() + { + FolderOrFileName = "file2.txt", + Size = 180, + DownloadLink = "https://fake.url/file2.txt" + } + ] + } + ]; + + mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny())).ReturnsAsync(files); + mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, "file1.txt", 123)).Returns(true); + mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, "folder/file2.txt", 180)).Returns(false); + + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); + + // Act + var result = await allDebridTorrentClient.GetDownloadLinks(torrent); + + // Assert + Assert.NotNull(result); + Assert.Single(result); + Assert.Contains("https://fake.url/file1.txt", result); + } + + [Fact] + public async Task GetDownloadLinks_WhenAllFilesExcluded_ReturnsAllFiles() { // Arrange var mocks = new Mocks(); @@ -494,20 +491,22 @@ public class AllDebridTorrentClientTest new() { FolderOrFileName = "file-1.txt", - Size = 50, + Size = 100, DownloadLink = "https://fake.url/file-1.txt" }, new() { FolderOrFileName = "file-2.txt", - Size = 150, + Size = 100, DownloadLink = "https://fake.url/file-2.txt" } ]; var expectedLinksSet = new HashSet(files.Select(n => n.DownloadLink)!); mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny())).ReturnsAsync(files); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); + mocks.FileFilterMock.Setup(f => f.IsDownloadable(torrent, It.IsAny(), It.IsAny())).Returns(false); + + var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object, mocks.FileFilterMock.Object); // Act var result = await allDebridTorrentClient.GetDownloadLinks(torrent); @@ -516,184 +515,9 @@ public class AllDebridTorrentClientTest Assert.NotNull(result); var linksSet = new HashSet(result); Assert.Equal(expectedLinksSet, linksSet); - } - [Fact] - public async Task GetDownloadLinks_WhenDownloadMinSizeSet_GetsLinksForOnlyFilesAboveThatSize() - { - // Arrange - var mocks = new Mocks(); - - var torrent = new Torrent - { - RdId = "1", - - // NB: this is in MB, the file sizes below are in B - DownloadMinSize = 100 - }; - - List files = - [ - new() - { - FolderOrFileName = "too-small.txt", - Size = (torrent.DownloadMinSize - 1) * 1024 * 1024, - DownloadLink = "https://fake.url/too-small.txt" - }, - new() - { - FolderOrFileName = "bigger-than-min.txt", - Size = (torrent.DownloadMinSize + 1) * 1024 * 1024, - DownloadLink = "https://fake.url/bigger-than-min.txt" - } - ]; - - var expectedLinksSet = new HashSet(files.Where(m => m.Size > torrent.DownloadMinSize * 1024 * 1024).Select(n => n.DownloadLink)!); - mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny())).ReturnsAsync(files); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); - - // Act - var result = await allDebridTorrentClient.GetDownloadLinks(torrent); - - // Assert - Assert.NotNull(result); - var linksSet = new HashSet(result); - Assert.Equal(expectedLinksSet, linksSet); - } - - [Fact] - public async Task GetDownloadLinks_WhenIncludeRegexSet_GetsLinksForOnlyFilesMatchingRegex() - { - // Arrange - var mocks = new Mocks(); - - var torrent = new Torrent - { - RdId = "1", - IncludeRegex = "include" - }; - - var expectedLinksSet = new HashSet([ - "https://fake.url/include.txt", "https://fake.url/include-folder-exclude-file.txt", "https://fake.url/include-folder-include-file.txt", - "https://fake.url/exclude-folder-include-file.txt" - ]); - - mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny())).ReturnsAsync(IncludeExcludeFiles); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); - - // Act - var result = await allDebridTorrentClient.GetDownloadLinks(torrent); - - // Assert - Assert.NotNull(result); - var linksSet = new HashSet(result); - Assert.Equal(expectedLinksSet, linksSet); - } - - [Fact] - public async Task GetDownloadLinks_WhenExcludeRegexSet_GetsLinksForOnlyFilesNotMatchingRegex() - { - // Arrange - var mocks = new Mocks(); - - var torrent = new Torrent - { - RdId = "1", - ExcludeRegex = "exclude" - }; - - var expectedLinksSet = new HashSet([ - "https://fake.url/include.txt", "https://fake.url/include-folder-include-file.txt" - ]); - - mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny())).ReturnsAsync(IncludeExcludeFiles); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); - - // Act - var result = await allDebridTorrentClient.GetDownloadLinks(torrent); - - // Assert - Assert.NotNull(result); - var linksSet = new HashSet(result); - Assert.Equal(expectedLinksSet, linksSet); - } - - [Fact] - public async Task GetDownloadLinks_WhenIncludeAndExcludeRegexSet_GetsLinksForOnlyFilesMatchingIncludeRegexEvenIfNotMatchingExcludeRegex() - { - // Arrange - var mocks = new Mocks(); - - var torrentInclude = new Torrent - { - RdId = "1", - IncludeRegex = "include" - }; - - var torrentIncludeExclude = new Torrent - { - RdId = "2", - IncludeRegex = "include", - ExcludeRegex = "exclude" - }; - - mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(It.IsAny(), It.IsAny())).ReturnsAsync(IncludeExcludeFiles); - - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); - - // Act - var includeOnlyResult = await allDebridTorrentClient.GetDownloadLinks(torrentInclude); - var includeExcludeResult = await allDebridTorrentClient.GetDownloadLinks(torrentIncludeExclude); - - // Assert - Assert.NotNull(includeOnlyResult); - Assert.NotNull(includeExcludeResult); - var includeOnlyLinksSet = new HashSet(includeOnlyResult); - var includeExcludeLinksSet = new HashSet(includeExcludeResult); - Assert.Equal(includeOnlyLinksSet, includeExcludeLinksSet); - } - - [Fact] - public async Task GetDownloadLinks_WhenAllFilesExcluded_ReturnsAllFiles() - { - // Arrange - var mocks = new Mocks(); - - var torrent = new Torrent - { - RdId = "1", - - // NB: this is in MB, the file sizes below are in B - DownloadMinSize = 100 - }; - - List files = - [ - new() - { - FolderOrFileName = "too-small-1.txt", - Size = (torrent.DownloadMinSize - 1) * 1024 * 1024, - DownloadLink = "https://fake.url/too-small-1.txt" - }, - new() - { - FolderOrFileName = "too-small-2.txt", - Size = (torrent.DownloadMinSize - 1) * 1024 * 1024, - DownloadLink = "https://fake.url/too-small-2.txt" - } - ]; - - var expectedLinksSet = new HashSet(files.Select(n => n.DownloadLink)!); - mocks.AllDebridClientMock.Setup(c => c.Magnet.FilesAsync(Int64.Parse(torrent.RdId), It.IsAny())).ReturnsAsync(files); - var allDebridTorrentClient = new AllDebridTorrentClient(mocks.LoggerMock.Object, mocks.AllDebridClientFactoryMock.Object); - - // Act - var result = await allDebridTorrentClient.GetDownloadLinks(torrent); - - // Assert - Assert.NotNull(result); - var linksSet = new HashSet(result); - Assert.Equal(expectedLinksSet, linksSet); + mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-1.txt", 100)); + mocks.FileFilterMock.Verify(f => f.IsDownloadable(torrent, "file-2.txt", 100)); } private class Mocks @@ -701,10 +525,12 @@ public class AllDebridTorrentClientTest public readonly Mock AllDebridClientFactoryMock; public readonly Mock AllDebridClientMock; public readonly Mock> LoggerMock; + public readonly Mock FileFilterMock; public Mocks() { LoggerMock = new(); + FileFilterMock = new(); AllDebridClientMock = new(); AllDebridClientFactoryMock = new(); AllDebridClientFactoryMock.Setup(f => f.GetClient()).Returns(AllDebridClientMock.Object); diff --git a/server/RdtClient.Service/DiConfig.cs b/server/RdtClient.Service/DiConfig.cs index 6035b28..e22e841 100644 --- a/server/RdtClient.Service/DiConfig.cs +++ b/server/RdtClient.Service/DiConfig.cs @@ -37,6 +37,8 @@ public static class DiConfig services.AddScoped(); services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(); diff --git a/server/RdtClient.Service/Services/DownloadableFileFilter.cs b/server/RdtClient.Service/Services/DownloadableFileFilter.cs new file mode 100644 index 0000000..810ec63 --- /dev/null +++ b/server/RdtClient.Service/Services/DownloadableFileFilter.cs @@ -0,0 +1,79 @@ +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using RdtClient.Data.Enums; +using RdtClient.Data.Models.Data; + +namespace RdtClient.Service.Services; + +public interface IDownloadableFileFilter +{ + public Boolean IsDownloadable(Torrent torrent, String filePath, Int64 fileSize); +} + +public class DownloadableFileFilter(ILogger logger) : IDownloadableFileFilter +{ + public Boolean IsDownloadable(Torrent torrent, String filePath, Int64 fileSize) + { + var isDownloadable = PassesSizeFilter(torrent, filePath, fileSize) && + PassesFilePathFilter(torrent, filePath); + + if (isDownloadable) + { + logger.LogDebug("File {filePath} was included after filtering", filePath); + } + + return isDownloadable; + } + + private Boolean PassesSizeFilter(Torrent torrent, String filePath, Int64 fileSize) + { + if (torrent is { ClientKind: Provider.RealDebrid, DownloadAction: TorrentDownloadAction.DownloadManual }) + { + return true; + } + + if (torrent.DownloadMinSize <= 0 || fileSize > torrent.DownloadMinSize * 1024 * 1024) + { + return true; + } + + logger.LogDebug("Not downloading file {filePath} file size {fileSize} smaller than minimum {downloadMinSize}", filePath, fileSize, torrent.DownloadMinSize); + + return false; + } + + private Boolean PassesFilePathFilter(Torrent torrent, String filePath) + { + return PassesIncludeRegexFilter(torrent, filePath) && PassesExcludeRegexFilter(torrent, filePath); + } + + private Boolean PassesIncludeRegexFilter(Torrent torrent, String filePath) + { + if (String.IsNullOrWhiteSpace(torrent.IncludeRegex) || Regex.IsMatch(filePath, torrent.IncludeRegex)) + { + return true; + } + + logger.LogDebug("Not downloading file {filePath} does not match regex {includeRegex}", filePath, torrent.IncludeRegex); + + return false; + } + + private Boolean PassesExcludeRegexFilter(Torrent torrent, String filePath) + { + // If the IncludeRegex is set, ignore the ExcludeRegex + if (!String.IsNullOrWhiteSpace(torrent.IncludeRegex)) + { + return true; + } + + if (String.IsNullOrWhiteSpace(torrent.ExcludeRegex) || !Regex.IsMatch(filePath, torrent.ExcludeRegex)) + { + return true; + } + + logger.LogDebug("Not downloading file {filePath} matches regex {excludeRegex}", filePath, torrent.ExcludeRegex); + + return false; + } +} diff --git a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs index 546217c..b1415cc 100644 --- a/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/AllDebridTorrentClient.cs @@ -1,5 +1,4 @@ -using System.Text.RegularExpressions; -using AllDebridNET; +using AllDebridNET; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using RdtClient.Data.Enums; @@ -52,7 +51,7 @@ public class AllDebridNetClientFactory(ILogger logger } } -public class AllDebridTorrentClient(ILogger logger, IAllDebridNetClientFactory allDebridNetClientFactory) : ITorrentClient +public class AllDebridTorrentClient(ILogger logger, IAllDebridNetClientFactory allDebridNetClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient { private static readonly Int64 SessionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); private static List _cache = []; @@ -257,67 +256,10 @@ public class AllDebridTorrentClient(ILogger logger, IAll var files = GetFiles(allFiles); + files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList(); + Log($"Getting download links", torrent); - if (torrent.DownloadMinSize > 0) - { - var minFileSize = torrent.DownloadMinSize * 1024 * 1024; - - Log($"Determining which files are over {minFileSize} bytes", torrent); - - files = files.Where(m => m.Bytes > minFileSize) - .ToList(); - - Log($"Found {files.Count} files that match the minimum file size criterea", torrent); - } - - if (!String.IsNullOrWhiteSpace(torrent.IncludeRegex)) - { - Log($"Using regular expression {torrent.IncludeRegex} to include only files matching this regex", torrent); - - var newFiles = new List(); - - foreach (var file in files) - { - if (Regex.IsMatch(file.Path, torrent.IncludeRegex)) - { - Log($"* Including {file.Path}", torrent); - newFiles.Add(file); - } - else - { - Log($"* Excluding {file.Path}", torrent); - } - } - - files = newFiles; - - Log($"Found {newFiles.Count} files that match the regex", torrent); - } - else if (!String.IsNullOrWhiteSpace(torrent.ExcludeRegex)) - { - Log($"Using regular expression {torrent.IncludeRegex} to ignore files matching this regex", torrent); - - var newLinks = new List(); - - foreach (var link in files) - { - if (!Regex.IsMatch(link.Path, torrent.ExcludeRegex)) - { - Log($"* Including {link.Path}", torrent); - newLinks.Add(link); - } - else - { - Log($"* Excluding {link.Path}", torrent); - } - } - - files = newLinks; - - Log($"Found {newLinks.Count} files that match the regex", torrent); - } - if (files.Count == 0) { Log($"Filtered all files out! Downloading ALL files instead!", torrent); diff --git a/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs index 385fec6..a4c4026 100644 --- a/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/DebridLinkTorrentClient.cs @@ -10,7 +10,7 @@ using Download = RdtClient.Data.Models.Data.Download; namespace RdtClient.Service.Services.TorrentClients; -public class DebridLinkClient(ILogger logger, IHttpClientFactory httpClientFactory) : ITorrentClient +public class DebridLinkClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient { private DebridLinkFrNETClient GetClient() { @@ -69,12 +69,14 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto Status = torrent.Status.ToString(), Added = DateTimeOffset.FromUnixTimeSeconds(torrent.Created), Files = (torrent.Files ?? []).Select((m, i) => new TorrentClientFile - { - Path = m.Name ?? "", - Bytes = m.Size, - Id = i, - Selected = true - }).ToList(), + { + Path = m.Name ?? "", + Bytes = m.Size, + Id = i, + Selected = true, + DownloadLink = m.DownloadUrl + }) + .ToList(), Links = torrent.Files?.Select(m => m.DownloadUrl.ToString()).ToList(), Ended = null, Speed = torrent.UploadSpeed, @@ -246,7 +248,10 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto return null; } - var downloadLinks = rdTorrent.Links.Where(m => !String.IsNullOrWhiteSpace(m)).ToList(); + var downloadLinks = rdTorrent.Files? + .Where(m => fileFilter.IsDownloadable(torrent, m.Path, m.Bytes) && m.DownloadLink != null) + .Select(m => m.DownloadLink!) + .ToList() ?? []; Log($"Found {downloadLinks.Count} links", torrent); @@ -255,25 +260,7 @@ public class DebridLinkClient(ILogger logger, IHttpClientFacto Log($"{link}", torrent); } - // Check if all the links are set that have been selected - if (torrent.Files.Count(m => m.Selected) == downloadLinks.Count) - { - return downloadLinks; - } - - // Check if all all the links are set for manual selection - if (torrent.ManualFiles.Count == downloadLinks.Count) - { - return downloadLinks; - } - - // If there is only 1 link, delay for 1 minute to see if more links pop up. - if (downloadLinks.Count == 1 && torrent.RdEnded.HasValue && DateTime.UtcNow > torrent.RdEnded.Value.ToUniversalTime().AddMinutes(1)) - { - return downloadLinks; - } - - return null; + return downloadLinks; } private async Task GetInfo(String torrentId) diff --git a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs index f40b992..5682002 100644 --- a/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/PremiumizeTorrentClient.cs @@ -1,5 +1,4 @@ -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using PremiumizeNET; using RdtClient.Data.Enums; @@ -10,7 +9,7 @@ using Torrent = RdtClient.Data.Models.Data.Torrent; namespace RdtClient.Service.Services.TorrentClients; -public class PremiumizeTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory) : ITorrentClient +public class PremiumizeTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient { private PremiumizeNETClient GetClient() { @@ -294,33 +293,9 @@ public class PremiumizeTorrentClient(ILogger logger, IH continue; } - if (torrent.DownloadMinSize > 0) + if (!fileFilter.IsDownloadable(torrent, item.Name, item.Size)) { - if (item.Link.Length < torrent.DownloadMinSize * 1024 * 1024) - { - Log($"Not downloading {item.Name}, its size of {item.Link.Length} bytes is smaller than the minimum size of {torrent.DownloadMinSize} bytes", torrent); - - continue; - } - } - - if (!String.IsNullOrWhiteSpace(torrent.IncludeRegex)) - { - if (!Regex.IsMatch(item.Name, torrent.IncludeRegex)) - { - Log($"Not downloading {item.Name}, it does not match regex {torrent.IncludeRegex}", torrent); - - continue; - } - } - else if (!String.IsNullOrWhiteSpace(torrent.ExcludeRegex)) - { - if (Regex.IsMatch(item.Name, torrent.ExcludeRegex)) - { - Log($"Not downloading {item.Name}, it matches regex {torrent.ExcludeRegex}", torrent); - - continue; - } + continue; } Log($"Found item {item.Name} in folder {folder.Name} ({folderId})", torrent); @@ -329,6 +304,12 @@ public class PremiumizeTorrentClient(ILogger logger, IH } else if (item.Type == "folder") { + // Folders don't have Size, use maximum Int64 so it always passes min size check + if (!fileFilter.IsDownloadable(torrent, item.Name, Int64.MaxValue)) + { + continue; + } + Log($"Found subfolder {item.Name} in {folder.Name} ({folderId}), searching subfolder for files", torrent); var subDownloadLinks = await GetAllDownloadLinks(torrent, item.Id); downloadLinks.AddRange(subDownloadLinks); diff --git a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs index 368e733..5baa7e3 100644 --- a/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/RealDebridTorrentClient.cs @@ -1,5 +1,4 @@ -using System.Text.RegularExpressions; -using System.Web; +using System.Web; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using RDNET; @@ -9,7 +8,7 @@ using RdtClient.Service.Helpers; namespace RdtClient.Service.Services.TorrentClients; -public class RealDebridTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory) : ITorrentClient +public class RealDebridTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient { private TimeSpan? _offset; @@ -162,62 +161,7 @@ public class RealDebridTorrentClient(ILogger logger, IH Log($"Selecting {files.Count}/{torrent.Files.Count} files", torrent); - if (torrent.DownloadAction != TorrentDownloadAction.DownloadManual && torrent.DownloadMinSize > 0) - { - var minFileSize = torrent.DownloadMinSize * 1024 * 1024; - - Log($"Determining which files are over {minFileSize} bytes", torrent); - - files = files.Where(m => m.Bytes > minFileSize) - .ToList(); - - Log($"Found {files.Count} files that match the minimum file size criterea", torrent); - } - - if (!String.IsNullOrWhiteSpace(torrent.IncludeRegex)) - { - Log($"Using regular expression {torrent.IncludeRegex} to include only files matching this regex", torrent); - - var newFiles = new List(); - foreach (var file in files) - { - if (Regex.IsMatch(file.Path, torrent.IncludeRegex)) - { - Log($"* Including {file.Path}", torrent); - newFiles.Add(file); - } - else - { - Log($"* Excluding {file.Path}", torrent); - } - } - - files = newFiles; - - Log($"Found {files.Count} files that match the regex", torrent); - } - else if (!String.IsNullOrWhiteSpace(torrent.ExcludeRegex)) - { - Log($"Using regular expression {torrent.IncludeRegex} to ignore files matching this regex", torrent); - - var newFiles = new List(); - foreach (var file in files) - { - if (!Regex.IsMatch(file.Path, torrent.ExcludeRegex)) - { - Log($"* Including {file.Path}", torrent); - newFiles.Add(file); - } - else - { - Log($"* Excluding {file.Path}", torrent); - } - } - - files = newFiles; - - Log($"Found {files.Count} files that match the regex", torrent); - } + files = files.Where(f => fileFilter.IsDownloadable(torrent, f.Path, f.Bytes)).ToList(); if (files.Count == 0) { diff --git a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs index cc0a6ce..5a7d95a 100644 --- a/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs +++ b/server/RdtClient.Service/Services/TorrentClients/TorBoxTorrentClient.cs @@ -1,4 +1,3 @@ -using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using TorBoxNET; @@ -10,7 +9,7 @@ using RdtClient.Service.Helpers; namespace RdtClient.Service.Services.TorrentClients; -public class TorBoxTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory) : ITorrentClient +public class TorBoxTorrentClient(ILogger logger, IHttpClientFactory httpClientFactory, IDownloadableFileFilter fileFilter) : ITorrentClient { private TimeSpan? _offset; private TorBoxNetClient GetClient() @@ -288,46 +287,9 @@ public class TorBoxTorrentClient(ILogger logger, IHttpClien { var torrentId = await GetClient().Torrents.GetHashInfoAsync(torrent.Hash, skipCache: true); - var selectedFiles = torrent.Files.Where(file => - { - var fileName = Path.GetFileName(file.Path); - - if (torrent.DownloadMinSize > 0 && file.Bytes < torrent.DownloadMinSize * 1024 * 1024) - { - Log($"Not downloading {fileName}, its size of {file.Bytes} bytes is smaller than the minimum size of {torrent.DownloadMinSize} bytes", torrent); - - return false; - } - - if (!String.IsNullOrWhiteSpace(torrent.IncludeRegex)) - { - if (!Regex.IsMatch(fileName, torrent.IncludeRegex)) - { - Log($"Not downloading {fileName}, it does not match regex {torrent.IncludeRegex}", torrent); - - return false; - } - - // If IncludeRegex is set, don't look at ExcludeRegex - return true; - } - - if (!String.IsNullOrWhiteSpace(torrent.ExcludeRegex)) - { - if (Regex.IsMatch(fileName, torrent.ExcludeRegex)) - - { - Log($"Not downloading {fileName}, it matches regex {torrent.ExcludeRegex}", torrent); - - return false; - } - } - - return true; - }); - - return selectedFiles.Select(file => $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}") - .ToList(); + return torrent.Files.Where(file => fileFilter.IsDownloadable(torrent, file.Path, file.Bytes)) + .Select(file => $"https://torbox.app/fakedl/{torrentId?.Id}/{file.Id}") + .ToList(); } public async Task GetFileName(String downloadUrl) diff --git a/server/RdtClient.Service/Services/Torrents.cs b/server/RdtClient.Service/Services/Torrents.cs index 3b2bb21..a88087a 100644 --- a/server/RdtClient.Service/Services/Torrents.cs +++ b/server/RdtClient.Service/Services/Torrents.cs @@ -242,6 +242,20 @@ public class Torrents( return; } + if (downloadLinks.Count == 0) + { + logger.LogInformation("All files excluded by filters (IncludeRegex: {includeRegex}, ExcludeRegex: {excludeRegex}, DownloadMinSize: {downloadMinSize} {torrentInfo}", + torrent.IncludeRegex, + torrent.ExcludeRegex, + torrent.DownloadMinSize, + torrent.ToLog()); + + await torrentData.UpdateRetry(torrentId, null, torrent.TorrentRetryAttempts); + await torrentData.UpdateComplete(torrentId, "All files excluded", DateTimeOffset.Now, false); + + return; + } + foreach (var downloadLink in downloadLinks) { // Make sure downloads don't get added multiple times