diff --git a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/SeedingRuleRequest.cs b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/SeedingRuleRequest.cs
new file mode 100644
index 00000000..fdbc408b
--- /dev/null
+++ b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/SeedingRuleRequest.cs
@@ -0,0 +1,29 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
+
+public record SeedingRuleRequest
+{
+ [Required]
+ public string Name { get; init; } = string.Empty;
+
+ ///
+ /// Max ratio before removing a download.
+ ///
+ public double MaxRatio { get; init; } = -1;
+
+ ///
+ /// Min number of hours to seed before removing a download, if the ratio has been met.
+ ///
+ public double MinSeedTime { get; init; }
+
+ ///
+ /// Number of hours to seed before removing a download.
+ ///
+ public double MaxSeedTime { get; init; } = -1;
+
+ ///
+ /// Whether to delete the source files when cleaning the download.
+ ///
+ public bool DeleteSourceFiles { get; init; } = true;
+}
\ No newline at end of file
diff --git a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/UpdateDownloadCleanerConfigRequest.cs b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/UpdateDownloadCleanerConfigRequest.cs
index ec5379ba..a2cee629 100644
--- a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/UpdateDownloadCleanerConfigRequest.cs
+++ b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/UpdateDownloadCleanerConfigRequest.cs
@@ -1,8 +1,6 @@
-using System.ComponentModel.DataAnnotations;
-
namespace Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
-public record UpdateDownloadCleanerConfigRequest
+public sealed record UpdateDownloadCleanerConfigRequest
{
public bool Enabled { get; init; }
@@ -13,7 +11,7 @@ public record UpdateDownloadCleanerConfigRequest
///
public bool UseAdvancedScheduling { get; init; }
- public List Categories { get; init; } = [];
+ public List Categories { get; init; } = [];
public bool DeletePrivate { get; init; }
@@ -32,24 +30,3 @@ public record UpdateDownloadCleanerConfigRequest
public List IgnoredDownloads { get; init; } = [];
}
-
-public record CleanCategoryRequest
-{
- [Required]
- public string Name { get; init; } = string.Empty;
-
- ///
- /// Max ratio before removing a download.
- ///
- public double MaxRatio { get; init; } = -1;
-
- ///
- /// Min number of hours to seed before removing a download, if the ratio has been met.
- ///
- public double MinSeedTime { get; init; }
-
- ///
- /// Number of hours to seed before removing a download.
- ///
- public double MaxSeedTime { get; init; } = -1;
-}
diff --git a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/DownloadCleanerConfigController.cs b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/DownloadCleanerConfigController.cs
index c6d8f480..b1bab330 100644
--- a/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/DownloadCleanerConfigController.cs
+++ b/code/backend/Cleanuparr.Api/Features/DownloadCleaner/Controllers/DownloadCleanerConfigController.cs
@@ -85,17 +85,18 @@ public sealed class DownloadCleanerConfigController : ControllerBase
oldConfig.IgnoredDownloads = newConfigDto.IgnoredDownloads;
oldConfig.Categories.Clear();
- _dataContext.CleanCategories.RemoveRange(oldConfig.Categories);
+ _dataContext.SeedingRules.RemoveRange(oldConfig.Categories);
_dataContext.DownloadCleanerConfigs.Update(oldConfig);
foreach (var categoryDto in newConfigDto.Categories)
{
- _dataContext.CleanCategories.Add(new CleanCategory
+ _dataContext.SeedingRules.Add(new SeedingRule
{
Name = categoryDto.Name,
MaxRatio = categoryDto.MaxRatio,
MinSeedTime = categoryDto.MinSeedTime,
MaxSeedTime = categoryDto.MaxSeedTime,
+ DeleteSourceFiles = categoryDto.DeleteSourceFiles,
DownloadCleanerConfigId = oldConfig.Id
});
}
diff --git a/code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs b/code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs
deleted file mode 100644
index 8a595609..00000000
--- a/code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-
-using Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
-
-namespace Cleanuparr.Api.Models;
-
-///
-/// Legacy namespace shim; prefer from
-/// Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests.
-///
-[Obsolete("Use Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests.UpdateDownloadCleanerConfigRequest instead")]
-[SuppressMessage("Design", "CA1000", Justification = "Temporary alias during refactor")]
-[SuppressMessage("Usage", "CA2225", Justification = "Alias type")]
-public record UpdateDownloadCleanerConfigDto : UpdateDownloadCleanerConfigRequest;
-
-///
-/// Legacy namespace shim; prefer from
-/// Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests.
-///
-[Obsolete("Use Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests.CleanCategoryRequest instead")]
-[SuppressMessage("Design", "CA1000", Justification = "Temporary alias during refactor")]
-[SuppressMessage("Usage", "CA2225", Justification = "Alias type")]
-public record CleanCategoryDto : CleanCategoryRequest;
\ No newline at end of file
diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceDCTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceDCTests.cs
index 0af7682b..e159e6c2 100644
--- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceDCTests.cs
+++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/DelugeServiceDCTests.cs
@@ -133,10 +133,10 @@ public class DelugeServiceDCTests : IClassFixture
new DelugeItemWrapper(new DownloadStatus { Hash = "hash3", Label = "music", Trackers = new List(), DownloadLocation = "/downloads" })
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 },
- new CleanCategory { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
+ new SeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -160,9 +160,9 @@ public class DelugeServiceDCTests : IClassFixture
new DelugeItemWrapper(new DownloadStatus { Hash = "hash1", Label = "Movies", Trackers = new List(), DownloadLocation = "/downloads" })
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -184,9 +184,9 @@ public class DelugeServiceDCTests : IClassFixture
new DelugeItemWrapper(new DownloadStatus { Hash = "hash1", Label = "music", Trackers = new List(), DownloadLocation = "/downloads" })
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -342,15 +342,15 @@ public class DelugeServiceDCTests : IClassFixture
const string hash = "TEST-HASH";
_fixture.ClientWrapper
- .Setup(x => x.DeleteTorrents(It.Is>(h => h.Contains("test-hash"))))
+ .Setup(x => x.DeleteTorrents(It.Is>(h => h.Contains("test-hash")), true))
.Returns(Task.CompletedTask);
// Act
- await sut.DeleteDownload(hash);
+ await sut.DeleteDownload(hash, true);
// Assert
_fixture.ClientWrapper.Verify(
- x => x.DeleteTorrents(It.Is>(h => h.Contains("test-hash"))),
+ x => x.DeleteTorrents(It.Is>(h => h.Contains("test-hash")), true),
Times.Once);
}
@@ -362,15 +362,35 @@ public class DelugeServiceDCTests : IClassFixture
const string hash = "UPPERCASE-HASH";
_fixture.ClientWrapper
- .Setup(x => x.DeleteTorrents(It.IsAny>()))
+ .Setup(x => x.DeleteTorrents(It.IsAny>(), true))
.Returns(Task.CompletedTask);
// Act
- await sut.DeleteDownload(hash);
+ await sut.DeleteDownload(hash, true);
// Assert
_fixture.ClientWrapper.Verify(
- x => x.DeleteTorrents(It.Is>(h => h.Contains("uppercase-hash"))),
+ x => x.DeleteTorrents(It.Is>(h => h.Contains("uppercase-hash")), true),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task CallsClientDeleteWithoutSourceFiles()
+ {
+ // Arrange
+ var sut = _fixture.CreateSut();
+ const string hash = "TEST-HASH";
+
+ _fixture.ClientWrapper
+ .Setup(x => x.DeleteTorrents(It.Is>(h => h.Contains("test-hash")), false))
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await sut.DeleteDownload(hash, false);
+
+ // Assert
+ _fixture.ClientWrapper.Verify(
+ x => x.DeleteTorrents(It.Is>(h => h.Contains("test-hash")), false),
Times.Once);
}
}
diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceDCTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceDCTests.cs
index 0c717ea1..f2fb4b51 100644
--- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceDCTests.cs
+++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/QBitServiceDCTests.cs
@@ -214,10 +214,10 @@ public class QBitServiceDCTests : IClassFixture
new QBitItemWrapper(new TorrentInfo { Hash = "hash3", Category = "music" }, Array.Empty(), false)
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 },
- new CleanCategory { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
+ new SeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -240,9 +240,9 @@ public class QBitServiceDCTests : IClassFixture
new QBitItemWrapper(new TorrentInfo { Hash = "hash1", Category = "Movies" }, Array.Empty(), false)
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -264,9 +264,9 @@ public class QBitServiceDCTests : IClassFixture
new QBitItemWrapper(new TorrentInfo { Hash = "hash1", Category = "movies" }, Array.Empty(), false)
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -288,9 +288,9 @@ public class QBitServiceDCTests : IClassFixture
new QBitItemWrapper(new TorrentInfo { Hash = "hash1", Category = "music" }, Array.Empty(), false)
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -509,7 +509,7 @@ public class QBitServiceDCTests : IClassFixture
.Returns(Task.CompletedTask);
// Act
- await sut.DeleteDownload(hash);
+ await sut.DeleteDownload(hash, true);
// Assert
_fixture.ClientWrapper.Verify(
@@ -529,7 +529,7 @@ public class QBitServiceDCTests : IClassFixture
.Returns(Task.CompletedTask);
// Act
- await sut.DeleteDownload(hash);
+ await sut.DeleteDownload(hash, true);
// Assert
_fixture.ClientWrapper.Verify(
diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceDCTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceDCTests.cs
index bd2d960f..d1322f3e 100644
--- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceDCTests.cs
+++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/DownloadClient/TransmissionServiceDCTests.cs
@@ -138,10 +138,10 @@ public class TransmissionServiceDCTests : IClassFixture
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 },
- new CleanCategory { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
+ new SeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -165,9 +165,9 @@ public class TransmissionServiceDCTests : IClassFixture
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -189,9 +189,9 @@ public class TransmissionServiceDCTests : IClassFixture
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -340,7 +340,7 @@ public class TransmissionServiceDCTests : IClassFixture
new UTorrentItemWrapper(new UTorrentItem { Hash = "hash3", Label = "music" }, new UTorrentProperties { Hash = "hash3", Pex = 1, Trackers = "" })
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 },
- new CleanCategory { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
+ new SeedingRule { Name = "tv", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -153,9 +153,9 @@ public class UTorrentServiceDCTests : IClassFixture
new UTorrentItemWrapper(new UTorrentItem { Hash = "hash1", Label = "Movies" }, new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" })
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -177,9 +177,9 @@ public class UTorrentServiceDCTests : IClassFixture
new UTorrentItemWrapper(new UTorrentItem { Hash = "hash1", Label = "music" }, new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" })
};
- var categories = new List
+ var categories = new List
{
- new CleanCategory { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = -1, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
};
// Act
@@ -292,15 +292,15 @@ public class UTorrentServiceDCTests : IClassFixture
const string hash = "TEST-HASH";
_fixture.ClientWrapper
- .Setup(x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("test-hash"))))
+ .Setup(x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("test-hash")), true))
.Returns(Task.CompletedTask);
// Act
- await sut.DeleteDownload(hash);
+ await sut.DeleteDownload(hash, true);
// Assert
_fixture.ClientWrapper.Verify(
- x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("test-hash"))),
+ x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("test-hash")), true),
Times.Once);
}
@@ -312,15 +312,35 @@ public class UTorrentServiceDCTests : IClassFixture
const string hash = "UPPERCASE-HASH";
_fixture.ClientWrapper
- .Setup(x => x.RemoveTorrentsAsync(It.IsAny>()))
+ .Setup(x => x.RemoveTorrentsAsync(It.IsAny>(), true))
.Returns(Task.CompletedTask);
// Act
- await sut.DeleteDownload(hash);
+ await sut.DeleteDownload(hash, true);
// Assert
_fixture.ClientWrapper.Verify(
- x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("uppercase-hash"))),
+ x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("uppercase-hash")), true),
+ Times.Once);
+ }
+
+ [Fact]
+ public async Task CallsClientDeleteWithoutSourceFiles()
+ {
+ // Arrange
+ var sut = _fixture.CreateSut();
+ const string hash = "TEST-HASH";
+
+ _fixture.ClientWrapper
+ .Setup(x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("test-hash")), false))
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await sut.DeleteDownload(hash, false);
+
+ // Assert
+ _fixture.ClientWrapper.Verify(
+ x => x.RemoveTorrentsAsync(It.Is>(h => h.Contains("test-hash")), false),
Times.Once);
}
}
diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs
index 83201091..c7b14e89 100644
--- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs
+++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/DownloadCleanerTests.cs
@@ -151,7 +151,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
var mockDownloadService = _fixture.CreateMockDownloadService();
mockDownloadService
@@ -185,7 +185,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
// Add ignored download to general config
var generalConfig = _fixture.DataContext.GeneralConfigs.First();
@@ -229,7 +229,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockTorrent = new Mock();
@@ -294,7 +294,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
@@ -312,7 +312,7 @@ public class DownloadCleanerTests : IDisposable
mockDownloadService
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.Returns([]);
@@ -419,7 +419,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext, "completed", 1.0, 60);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext, "completed", 1.0, 60);
var mockTorrent = new Mock();
mockTorrent.Setup(x => x.Hash).Returns("test-hash");
@@ -434,13 +434,13 @@ public class DownloadCleanerTests : IDisposable
mockDownloadService
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.Returns([mockTorrent.Object]);
mockDownloadService
.Setup(x => x.CleanDownloadsAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.Returns(Task.CompletedTask);
@@ -475,7 +475,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
// Need at least one download for arr processing to occur
@@ -492,7 +492,7 @@ public class DownloadCleanerTests : IDisposable
mockDownloadService
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.Returns([]);
@@ -548,7 +548,7 @@ public class DownloadCleanerTests : IDisposable
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext, "Failing Client");
TestDataContextFactory.AddDownloadClient(_fixture.DataContext, "Working Client");
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
var failingService = _fixture.CreateMockDownloadService("Failing Client");
failingService
@@ -754,7 +754,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
var mockTorrent = new Mock();
mockTorrent.Setup(x => x.Hash).Returns("test-hash");
@@ -769,7 +769,7 @@ public class DownloadCleanerTests : IDisposable
mockDownloadService
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.Throws(new Exception("Filter failed"));
@@ -800,7 +800,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
var mockTorrent = new Mock();
mockTorrent.Setup(x => x.Hash).Returns("test-hash");
@@ -815,13 +815,13 @@ public class DownloadCleanerTests : IDisposable
mockDownloadService
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.Returns([mockTorrent.Object]);
mockDownloadService
.Setup(x => x.CleanDownloadsAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.ThrowsAsync(new Exception("Clean failed"));
@@ -852,7 +852,7 @@ public class DownloadCleanerTests : IDisposable
{
// Arrange - DownloadCleaner calls ProcessArrConfigAsync with throwOnFailure=true
TestDataContextFactory.AddDownloadClient(_fixture.DataContext);
- TestDataContextFactory.AddCleanCategory(_fixture.DataContext);
+ TestDataContextFactory.AddSeedingRule(_fixture.DataContext);
TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
var mockTorrent = new Mock();
@@ -868,7 +868,7 @@ public class DownloadCleanerTests : IDisposable
mockDownloadService
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
It.IsAny>(),
- It.IsAny>()
+ It.IsAny>()
))
.Returns([]);
diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/TestHelpers/TestDataContextFactory.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/TestHelpers/TestDataContextFactory.cs
index a113bf17..742580b9 100644
--- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/TestHelpers/TestDataContextFactory.cs
+++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/TestHelpers/TestDataContextFactory.cs
@@ -308,7 +308,7 @@ public static class TestDataContextFactory
///
/// Adds a clean category to the download cleaner config
///
- public static CleanCategory AddCleanCategory(
+ public static SeedingRule AddSeedingRule(
DataContext context,
string name = "completed",
double maxRatio = 1.0,
@@ -316,18 +316,19 @@ public static class TestDataContextFactory
double maxSeedTime = -1)
{
var config = context.DownloadCleanerConfigs.Include(x => x.Categories).First();
- var category = new CleanCategory
+ var category = new SeedingRule
{
Id = Guid.NewGuid(),
Name = name,
MaxRatio = maxRatio,
MinSeedTime = minSeedTime,
MaxSeedTime = maxSeedTime,
+ DeleteSourceFiles = true,
DownloadCleanerConfigId = config.Id
};
config.Categories.Add(category);
- context.CleanCategories.Add(category);
+ context.SeedingRules.Add(category);
context.SaveChanges();
return category;
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs
index 171325f6..94bfd0af 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClient.cs
@@ -156,9 +156,9 @@ public sealed class DelugeClient
await SendRequest>("core.set_torrent_options", hash, filePriorities);
}
- public async Task DeleteTorrents(List hashes)
+ public async Task DeleteTorrents(List hashes, bool removeData)
{
- await SendRequest>("core.remove_torrents", hashes, true);
+ await SendRequest>("core.remove_torrents", hashes, removeData);
}
private async Task PostJson(String json)
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs
index e01aa2b7..d6656b10 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeClientWrapper.cs
@@ -35,8 +35,8 @@ public sealed class DelugeClientWrapper : IDelugeClientWrapper
public Task?> GetStatusForAllTorrents()
=> _client.GetStatusForAllTorrents();
- public Task DeleteTorrents(List hashes)
- => _client.DeleteTorrents(hashes);
+ public Task DeleteTorrents(List hashes, bool removeData)
+ => _client.DeleteTorrents(hashes, removeData);
public Task ChangeFilesPriority(string hash, List priorities)
=> _client.ChangeFilesPriority(hash, priorities);
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceDC.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceDC.cs
index 10f8fc57..037d7f3b 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceDC.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/DelugeServiceDC.cs
@@ -25,9 +25,9 @@ public partial class DelugeService
.ToList();
}
- public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List categories) =>
+ public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List seedingRules) =>
downloads
- ?.Where(x => categories.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
+ ?.Where(x => seedingRules.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
.ToList();
public override List? FilterDownloadsToChangeCategoryAsync(List? downloads, List categories) =>
@@ -37,9 +37,9 @@ public partial class DelugeService
.ToList();
///
- protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent)
+ protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent, bool deleteSourceFiles)
{
- await DeleteDownload(torrent.Hash);
+ await DeleteDownload(torrent.Hash, deleteSourceFiles);
}
public override async Task CreateCategoryAsync(string name)
@@ -142,11 +142,11 @@ public partial class DelugeService
}
///
- public override async Task DeleteDownload(string hash)
+ public override async Task DeleteDownload(string hash, bool deleteSourceFiles)
{
hash = hash.ToLowerInvariant();
-
- await _client.DeleteTorrents([hash]);
+
+ await _client.DeleteTorrents([hash], deleteSourceFiles);
}
protected async Task CreateLabel(string name)
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs
index 30cc0795..559d63c7 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Deluge/IDelugeClientWrapper.cs
@@ -12,7 +12,7 @@ public interface IDelugeClientWrapper
Task GetTorrent(string hash);
Task GetTorrentExtended(string hash);
Task?> GetStatusForAllTorrents();
- Task DeleteTorrents(List hashes);
+ Task DeleteTorrents(List hashes, bool removeData);
Task ChangeFilesPriority(string hash, List priorities);
Task> GetLabels();
Task CreateLabel(string label);
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs
index 8487235b..67ef95c8 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/DownloadService.cs
@@ -82,19 +82,19 @@ public abstract class DownloadService : IDownloadService
public abstract Task ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList ignoredDownloads);
///
- public abstract Task DeleteDownload(string hash);
+ public abstract Task DeleteDownload(string hash, bool deleteSourceFiles);
///
public abstract Task> GetSeedingDownloads();
///
- public abstract List? FilterDownloadsToBeCleanedAsync(List? downloads, List categories);
+ public abstract List? FilterDownloadsToBeCleanedAsync(List? downloads, List seedingRules);
///
public abstract List? FilterDownloadsToChangeCategoryAsync(List? downloads, List categories);
///
- public virtual async Task CleanDownloadsAsync(List? downloads, List categoriesToClean)
+ public virtual async Task CleanDownloadsAsync(List? downloads, List seedingRules)
{
if (downloads?.Count is null or 0)
{
@@ -108,7 +108,7 @@ public abstract class DownloadService : IDownloadService
continue;
}
- CleanCategory? category = categoriesToClean
+ SeedingRule? category = seedingRules
.FirstOrDefault(x => (torrent.Category ?? string.Empty).Equals(x.Name, StringComparison.InvariantCultureIgnoreCase));
if (category is null)
@@ -135,13 +135,14 @@ public abstract class DownloadService : IDownloadService
continue;
}
- await _dryRunInterceptor.InterceptAsync(DeleteDownloadInternal, torrent);
+ await _dryRunInterceptor.InterceptAsync(() => DeleteDownloadInternal(torrent, category.DeleteSourceFiles));
_logger.LogInformation(
- "download cleaned | {reason} reached | {name}",
+ "download cleaned | {reason} reached | delete files: {deleteFiles} | {name}",
result.Reason is CleanReason.MaxRatioReached
? "MAX_RATIO & MIN_SEED_TIME"
: "MAX_SEED_TIME",
+ category.DeleteSourceFiles,
torrent.Name
);
@@ -163,9 +164,10 @@ public abstract class DownloadService : IDownloadService
/// Each client implementation handles the deletion according to its API requirements.
///
/// The torrent to delete
- protected abstract Task DeleteDownloadInternal(ITorrentItemWrapper torrent);
+ /// Whether to delete the source files along with the torrent
+ protected abstract Task DeleteDownloadInternal(ITorrentItemWrapper torrent, bool deleteSourceFiles);
- protected SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, CleanCategory category)
+ protected SeedingCheckResult ShouldCleanDownload(double ratio, TimeSpan seedingTime, SeedingRule category)
{
// check ratio
if (DownloadReachedRatio(ratio, seedingTime, category))
@@ -210,7 +212,7 @@ public abstract class DownloadService : IDownloadService
return parts.Length > 0 ? Path.Combine(root, parts[0]) : root;
}
- private bool DownloadReachedRatio(double ratio, TimeSpan seedingTime, CleanCategory category)
+ private bool DownloadReachedRatio(double ratio, TimeSpan seedingTime, SeedingRule category)
{
if (category.MaxRatio < 0)
{
@@ -236,7 +238,7 @@ public abstract class DownloadService : IDownloadService
return true;
}
- private bool DownloadReachedMaxSeedTime(TimeSpan seedingTime, CleanCategory category)
+ private bool DownloadReachedMaxSeedTime(TimeSpan seedingTime, SeedingRule category)
{
if (category.MaxSeedTime < 0)
{
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/IDownloadService.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/IDownloadService.cs
index 92bfbc89..c112cb48 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/IDownloadService.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/IDownloadService.cs
@@ -36,9 +36,9 @@ public interface IDownloadService : IDisposable
/// Filters downloads that should be cleaned.
///
/// The downloads to filter.
- /// The categories by which to filter the downloads.
+ /// The seeding rules by which to filter the downloads.
/// A list of downloads for the provided categories.
- List? FilterDownloadsToBeCleanedAsync(List? downloads, List categories);
+ List? FilterDownloadsToBeCleanedAsync(List? downloads, List seedingRules);
///
/// Filters downloads that should have their category changed.
@@ -52,8 +52,8 @@ public interface IDownloadService : IDisposable
/// Cleans the downloads.
///
/// The downloads to clean.
- /// The categories that should be cleaned.
- Task CleanDownloadsAsync(List? downloads, List categoriesToClean);
+ /// The seeding rules.
+ Task CleanDownloadsAsync(List? downloads, List seedingRules);
///
/// Changes the category for downloads that have no hardlinks.
@@ -64,7 +64,9 @@ public interface IDownloadService : IDisposable
///
/// Deletes a download item.
///
- public Task DeleteDownload(string hash);
+ /// The torrent hash.
+ /// Whether to delete the source files along with the torrent. Defaults to true.
+ public Task DeleteDownload(string hash, bool deleteSourceFiles);
///
/// Creates a category.
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitServiceDC.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitServiceDC.cs
index 6c1411d3..bf051743 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitServiceDC.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/QBittorrent/QBitServiceDC.cs
@@ -33,10 +33,10 @@ public partial class QBitService
}
///
- public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List categories) =>
+ public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List seedingRules) =>
downloads
?.Where(x => !string.IsNullOrEmpty(x.Hash))
- .Where(x => categories.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
+ .Where(x => seedingRules.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
.ToList();
///
@@ -61,9 +61,9 @@ public partial class QBitService
}
///
- protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent)
+ protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent, bool deleteSourceFiles)
{
- await DeleteDownload(torrent.Hash);
+ await DeleteDownload(torrent.Hash, deleteSourceFiles);
}
public override async Task CreateCategoryAsync(string name)
@@ -175,9 +175,9 @@ public partial class QBitService
}
///
- public override async Task DeleteDownload(string hash)
+ public override async Task DeleteDownload(string hash, bool deleteSourceFiles)
{
- await _client.DeleteAsync([hash], deleteDownloadedData: true);
+ await _client.DeleteAsync([hash], deleteDownloadedData: deleteSourceFiles);
}
protected async Task CreateCategory(string name)
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionServiceDC.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionServiceDC.cs
index 0556c2f7..0da358e7 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionServiceDC.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/Transmission/TransmissionServiceDC.cs
@@ -21,10 +21,10 @@ public partial class TransmissionService
}
///
- public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List categories)
+ public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List seedingRules)
{
return downloads
- ?.Where(x => categories
+ ?.Where(x => seedingRules
.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase))
)
.ToList();
@@ -39,10 +39,10 @@ public partial class TransmissionService
}
///
- protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent)
+ protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent, bool deleteSourceFiles)
{
var transmissionTorrent = (TransmissionItemWrapper)torrent;
- await RemoveDownloadAsync(transmissionTorrent.Info.Id);
+ await RemoveDownloadAsync(transmissionTorrent.Info.Id, deleteSourceFiles);
}
public override async Task CreateCategoryAsync(string name)
@@ -140,7 +140,7 @@ public partial class TransmissionService
await _client.TorrentSetLocationAsync([downloadId], newLocation, true);
}
- public override async Task DeleteDownload(string hash)
+ public override async Task DeleteDownload(string hash, bool deleteSourceFiles)
{
TorrentInfo? torrent = await GetTorrentAsync(hash);
@@ -149,11 +149,11 @@ public partial class TransmissionService
return;
}
- await _client.TorrentRemoveAsync([torrent.Id], true);
+ await _client.TorrentRemoveAsync([torrent.Id], deleteSourceFiles);
}
-
- protected virtual async Task RemoveDownloadAsync(long downloadId)
+
+ protected virtual async Task RemoveDownloadAsync(long downloadId, bool deleteSourceFiles)
{
- await _client.TorrentRemoveAsync([downloadId], true);
+ await _client.TorrentRemoveAsync([downloadId], deleteSourceFiles);
}
}
\ No newline at end of file
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/IUTorrentClientWrapper.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/IUTorrentClientWrapper.cs
index 4d27d89b..42b5cf70 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/IUTorrentClientWrapper.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/IUTorrentClientWrapper.cs
@@ -13,5 +13,5 @@ public interface IUTorrentClientWrapper
Task> GetLabelsAsync();
Task SetTorrentLabelAsync(string hash, string label);
Task SetFilesPriorityAsync(string hash, List fileIndexes, int priority);
- Task RemoveTorrentsAsync(List hashes);
+ Task RemoveTorrentsAsync(List hashes, bool deleteData);
}
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClient.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClient.cs
index 55f271bd..1a2ebb49 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClient.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClient.cs
@@ -210,13 +210,16 @@ public sealed class UTorrentClient
/// Removes torrents from µTorrent
///
/// List of torrent hashes to remove
- public async Task RemoveTorrentsAsync(List hashes)
+ /// Whether to delete the downloaded data files
+ public async Task RemoveTorrentsAsync(List hashes, bool deleteData)
{
try
{
foreach (var hash in hashes)
{
- var request = UTorrentRequestFactory.CreateRemoveTorrentWithDataRequest(hash);
+ var request = deleteData
+ ? UTorrentRequestFactory.CreateRemoveTorrentWithDataRequest(hash)
+ : UTorrentRequestFactory.CreateRemoveTorrentRequest(hash);
await SendAuthenticatedRequestAsync(request);
}
}
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClientWrapper.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClientWrapper.cs
index 0c275458..49018c15 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClientWrapper.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentClientWrapper.cs
@@ -38,6 +38,6 @@ public sealed class UTorrentClientWrapper : IUTorrentClientWrapper
public Task SetFilesPriorityAsync(string hash, List fileIndexes, int priority)
=> _client.SetFilesPriorityAsync(hash, fileIndexes, priority);
- public Task RemoveTorrentsAsync(List hashes)
- => _client.RemoveTorrentsAsync(hashes);
+ public Task RemoveTorrentsAsync(List hashes, bool deleteData)
+ => _client.RemoveTorrentsAsync(hashes, deleteData);
}
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentRequestFactory.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentRequestFactory.cs
index b9a6f5b8..d6729802 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentRequestFactory.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentRequestFactory.cs
@@ -59,6 +59,17 @@ public static class UTorrentRequestFactory
.WithParameter("hash", hash);
}
+ ///
+ /// Creates a request to remove a torrent without deleting its data
+ ///
+ /// Torrent hash
+ /// Request for remove torrent API call
+ public static UTorrentRequest CreateRemoveTorrentRequest(string hash)
+ {
+ return UTorrentRequest.Create("action=removetorrent", string.Empty)
+ .WithParameter("hash", hash);
+ }
+
///
/// Creates a request to set file priorities for a torrent
///
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentServiceDC.cs b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentServiceDC.cs
index 0d94e3f9..6391a136 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentServiceDC.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/DownloadClient/UTorrent/UTorrentServiceDC.cs
@@ -24,9 +24,9 @@ public partial class UTorrentService
return result;
}
- public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List categories) =>
+ public override List? FilterDownloadsToBeCleanedAsync(List? downloads, List seedingRules) =>
downloads
- ?.Where(x => categories.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
+ ?.Where(x => seedingRules.Any(cat => cat.Name.Equals(x.Category, StringComparison.InvariantCultureIgnoreCase)))
.ToList();
public override List? FilterDownloadsToChangeCategoryAsync(List? downloads, List categories) =>
@@ -36,9 +36,9 @@ public partial class UTorrentService
.ToList();
///
- protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent)
+ protected override async Task DeleteDownloadInternal(ITorrentItemWrapper torrent, bool deleteSourceFiles)
{
- await DeleteDownload(torrent.Hash);
+ await DeleteDownload(torrent.Hash, deleteSourceFiles);
}
public override async Task CreateCategoryAsync(string name)
@@ -124,11 +124,11 @@ public partial class UTorrentService
}
///
- public override async Task DeleteDownload(string hash)
+ public override async Task DeleteDownload(string hash, bool deleteSourceFiles)
{
hash = hash.ToLowerInvariant();
-
- await _client.RemoveTorrentsAsync([hash]);
+
+ await _client.RemoveTorrentsAsync([hash], deleteSourceFiles);
}
protected virtual async Task ChangeLabel(string hash, string newLabel)
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/DownloadCleaner.cs b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/DownloadCleaner.cs
index 3dae0746..2f11b05d 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/DownloadCleaner.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/DownloadCleaner.cs
@@ -128,8 +128,6 @@ public sealed class DownloadCleaner : GenericHandler
await ChangeUnlinkedCategoriesAsync(isUnlinkedEnabled, downloadServiceToDownloadsMap, config);
await CleanDownloadsAsync(downloadServiceToDownloadsMap, config);
-
-
foreach (var downloadService in downloadServices)
{
diff --git a/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/DownloadCleanerConfigTests.cs b/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/DownloadCleanerConfigTests.cs
index 5856417a..d11ac41d 100644
--- a/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/DownloadCleanerConfigTests.cs
+++ b/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/DownloadCleanerConfigTests.cs
@@ -79,8 +79,8 @@ public sealed class DownloadCleanerConfigTests
Enabled = true,
Categories =
[
- new CleanCategory { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1 },
- new CleanCategory { Name = "tv", MaxRatio = 1.5, MinSeedTime = 24, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
+ new SeedingRule { Name = "tv", MaxRatio = 1.5, MinSeedTime = 24, MaxSeedTime = -1, DeleteSourceFiles = true }
],
UnlinkedEnabled = false
};
@@ -96,8 +96,8 @@ public sealed class DownloadCleanerConfigTests
Enabled = true,
Categories =
[
- new CleanCategory { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1 },
- new CleanCategory { Name = "movies", MaxRatio = 1.5, MinSeedTime = 24, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true },
+ new SeedingRule { Name = "movies", MaxRatio = 1.5, MinSeedTime = 24, MaxSeedTime = -1, DeleteSourceFiles = true }
],
UnlinkedEnabled = false
};
@@ -114,7 +114,7 @@ public sealed class DownloadCleanerConfigTests
Enabled = true,
Categories =
[
- new CleanCategory { Name = "", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
],
UnlinkedEnabled = false
};
@@ -151,7 +151,7 @@ public sealed class DownloadCleanerConfigTests
Enabled = true,
Categories =
[
- new CleanCategory { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
],
UnlinkedEnabled = true,
UnlinkedTargetCategory = "",
@@ -171,7 +171,7 @@ public sealed class DownloadCleanerConfigTests
Enabled = true,
Categories =
[
- new CleanCategory { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
],
UnlinkedEnabled = true,
UnlinkedTargetCategory = "cleanuparr-unlinked",
@@ -259,7 +259,7 @@ public sealed class DownloadCleanerConfigTests
Enabled = true,
Categories =
[
- new CleanCategory { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1 }
+ new SeedingRule { Name = "movies", MaxRatio = 2.0, MinSeedTime = 0, MaxSeedTime = -1, DeleteSourceFiles = true }
],
UnlinkedEnabled = true,
UnlinkedTargetCategory = "cleanuparr-unlinked",
diff --git a/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/CleanCategoryTests.cs b/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/SeedingRuleTests.cs
similarity index 73%
rename from code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/CleanCategoryTests.cs
rename to code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/SeedingRuleTests.cs
index a9914f49..5a3fbada 100644
--- a/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/CleanCategoryTests.cs
+++ b/code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/SeedingRuleTests.cs
@@ -5,19 +5,20 @@ using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException;
namespace Cleanuparr.Persistence.Tests.Models.Configuration.DownloadCleaner;
-public sealed class CleanCategoryTests
+public sealed class SeedingRuleTests
{
#region Validate - Valid Configurations
[Fact]
public void Validate_WithValidMaxRatio_DoesNotThrow()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = 2.0,
MinSeedTime = 0,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
Should.NotThrow(() => config.Validate());
@@ -26,12 +27,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithValidMaxSeedTime_DoesNotThrow()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = -1,
MinSeedTime = 0,
- MaxSeedTime = 24
+ MaxSeedTime = 24,
+ DeleteSourceFiles = true
};
Should.NotThrow(() => config.Validate());
@@ -40,12 +42,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithBothMaxRatioAndMaxSeedTime_DoesNotThrow()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = 2.0,
MinSeedTime = 1,
- MaxSeedTime = 48
+ MaxSeedTime = 48,
+ DeleteSourceFiles = true
};
Should.NotThrow(() => config.Validate());
@@ -54,12 +57,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithZeroMaxRatio_DoesNotThrow()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = 0,
MinSeedTime = 0,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
Should.NotThrow(() => config.Validate());
@@ -68,12 +72,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithZeroMaxSeedTime_DoesNotThrow()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = -1,
MinSeedTime = 0,
- MaxSeedTime = 0
+ MaxSeedTime = 0,
+ DeleteSourceFiles = true
};
Should.NotThrow(() => config.Validate());
@@ -86,12 +91,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithEmptyName_ThrowsValidationException()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "",
MaxRatio = 2.0,
MinSeedTime = 0,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
var exception = Should.Throw(() => config.Validate());
@@ -101,12 +107,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithWhitespaceName_ThrowsValidationException()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = " ",
MaxRatio = 2.0,
MinSeedTime = 0,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
var exception = Should.Throw(() => config.Validate());
@@ -116,12 +123,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithTabOnlyName_ThrowsValidationException()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "\t",
MaxRatio = 2.0,
MinSeedTime = 0,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
var exception = Should.Throw(() => config.Validate());
@@ -135,12 +143,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithBothNegative_ThrowsValidationException()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = -1,
MinSeedTime = 0,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
var exception = Should.Throw(() => config.Validate());
@@ -153,12 +162,13 @@ public sealed class CleanCategoryTests
[InlineData(-100, -100)]
public void Validate_WithVariousNegativeValues_ThrowsValidationException(double maxRatio, double maxSeedTime)
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = maxRatio,
MinSeedTime = 0,
- MaxSeedTime = maxSeedTime
+ MaxSeedTime = maxSeedTime,
+ DeleteSourceFiles = true
};
var exception = Should.Throw(() => config.Validate());
@@ -172,12 +182,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithNegativeMinSeedTime_ThrowsValidationException()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = 2.0,
MinSeedTime = -1,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
var exception = Should.Throw(() => config.Validate());
@@ -190,12 +201,13 @@ public sealed class CleanCategoryTests
[InlineData(-100)]
public void Validate_WithVariousNegativeMinSeedTime_ThrowsValidationException(double minSeedTime)
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = 2.0,
MinSeedTime = minSeedTime,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
var exception = Should.Throw(() => config.Validate());
@@ -205,12 +217,13 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithZeroMinSeedTime_DoesNotThrow()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = 2.0,
MinSeedTime = 0,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
Should.NotThrow(() => config.Validate());
@@ -219,16 +232,32 @@ public sealed class CleanCategoryTests
[Fact]
public void Validate_WithPositiveMinSeedTime_DoesNotThrow()
{
- var config = new CleanCategory
+ var config = new SeedingRule
{
Name = "test-category",
MaxRatio = 2.0,
MinSeedTime = 24,
- MaxSeedTime = -1
+ MaxSeedTime = -1,
+ DeleteSourceFiles = true
};
Should.NotThrow(() => config.Validate());
}
#endregion
+
+ [Fact]
+ public void DeleteSourceFiles_CanBeSetToFalse()
+ {
+ var config = new SeedingRule
+ {
+ Name = "test-category",
+ MaxRatio = 2.0,
+ MinSeedTime = 0,
+ MaxSeedTime = -1,
+ DeleteSourceFiles = false
+ };
+
+ config.DeleteSourceFiles.ShouldBeFalse();
+ }
}
diff --git a/code/backend/Cleanuparr.Persistence/DataContext.cs b/code/backend/Cleanuparr.Persistence/DataContext.cs
index 8d78c3fc..a842d6ea 100644
--- a/code/backend/Cleanuparr.Persistence/DataContext.cs
+++ b/code/backend/Cleanuparr.Persistence/DataContext.cs
@@ -38,7 +38,7 @@ public class DataContext : DbContext
public DbSet DownloadCleanerConfigs { get; set; }
- public DbSet CleanCategories { get; set; }
+ public DbSet SeedingRules { get; set; }
public DbSet ArrConfigs { get; set; }
diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.Designer.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.Designer.cs
new file mode 100644
index 00000000..61494423
--- /dev/null
+++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.Designer.cs
@@ -0,0 +1,1095 @@
+//
+using System;
+using System.Collections.Generic;
+using Cleanuparr.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Cleanuparr.Persistence.Migrations.Data
+{
+ [DbContext(typeof(DataContext))]
+ [Migration("20251216204347_AddDeleteSourceFilesToCleanCategory")]
+ partial class AddDeleteSourceFilesToCleanCategory
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("FailedImportMaxStrikes")
+ .HasColumnType("INTEGER")
+ .HasColumnName("failed_import_max_strikes");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("pk_arr_configs");
+
+ b.ToTable("arr_configs", (string)null);
+ });
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("ApiKey")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("api_key");
+
+ b.Property("ArrConfigId")
+ .HasColumnType("TEXT")
+ .HasColumnName("arr_config_id");
+
+ b.Property("Enabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("enabled");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("name");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("url");
+
+ b.HasKey("Id")
+ .HasName("pk_arr_instances");
+
+ b.HasIndex("ArrConfigId")
+ .HasDatabaseName("ix_arr_instances_arr_config_id");
+
+ b.ToTable("arr_instances", (string)null);
+ });
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.BlacklistSync.BlacklistSyncConfig", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("BlacklistPath")
+ .HasColumnType("TEXT")
+ .HasColumnName("blacklist_path");
+
+ b.Property("CronExpression")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("cron_expression");
+
+ b.Property("Enabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("enabled");
+
+ b.HasKey("Id")
+ .HasName("pk_blacklist_sync_configs");
+
+ b.ToTable("blacklist_sync_configs", (string)null);
+ });
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("DeleteSourceFiles")
+ .HasColumnType("INTEGER")
+ .HasColumnName("delete_source_files");
+
+ b.Property("DownloadCleanerConfigId")
+ .HasColumnType("TEXT")
+ .HasColumnName("download_cleaner_config_id");
+
+ b.Property("MaxRatio")
+ .HasColumnType("REAL")
+ .HasColumnName("max_ratio");
+
+ b.Property("MaxSeedTime")
+ .HasColumnType("REAL")
+ .HasColumnName("max_seed_time");
+
+ b.Property("MinSeedTime")
+ .HasColumnType("REAL")
+ .HasColumnName("min_seed_time");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("pk_clean_categories");
+
+ b.HasIndex("DownloadCleanerConfigId")
+ .HasDatabaseName("ix_clean_categories_download_cleaner_config_id");
+
+ b.ToTable("clean_categories", (string)null);
+ });
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("CronExpression")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("cron_expression");
+
+ b.Property("DeletePrivate")
+ .HasColumnType("INTEGER")
+ .HasColumnName("delete_private");
+
+ b.Property("Enabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("enabled");
+
+ b.PrimitiveCollection("IgnoredDownloads")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ignored_downloads");
+
+ b.PrimitiveCollection("UnlinkedCategories")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("unlinked_categories");
+
+ b.Property("UnlinkedEnabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("unlinked_enabled");
+
+ b.Property("UnlinkedIgnoredRootDir")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("unlinked_ignored_root_dir");
+
+ b.Property("UnlinkedTargetCategory")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("unlinked_target_category");
+
+ b.Property("UnlinkedUseTag")
+ .HasColumnType("INTEGER")
+ .HasColumnName("unlinked_use_tag");
+
+ b.Property("UseAdvancedScheduling")
+ .HasColumnType("INTEGER")
+ .HasColumnName("use_advanced_scheduling");
+
+ b.HasKey("Id")
+ .HasName("pk_download_cleaner_configs");
+
+ b.ToTable("download_cleaner_configs", (string)null);
+ });
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("Enabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("enabled");
+
+ b.Property("Host")
+ .HasColumnType("TEXT")
+ .HasColumnName("host");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("name");
+
+ b.Property("Password")
+ .HasColumnType("TEXT")
+ .HasColumnName("password");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("type");
+
+ b.Property("TypeName")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("type_name");
+
+ b.Property("UrlBase")
+ .HasColumnType("TEXT")
+ .HasColumnName("url_base");
+
+ b.Property("Username")
+ .HasColumnType("TEXT")
+ .HasColumnName("username");
+
+ b.HasKey("Id")
+ .HasName("pk_download_clients");
+
+ b.ToTable("download_clients", (string)null);
+ });
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("DisplaySupportBanner")
+ .HasColumnType("INTEGER")
+ .HasColumnName("display_support_banner");
+
+ b.Property("DryRun")
+ .HasColumnType("INTEGER")
+ .HasColumnName("dry_run");
+
+ b.Property("EncryptionKey")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("encryption_key");
+
+ b.Property("HttpCertificateValidation")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("http_certificate_validation");
+
+ b.Property("HttpMaxRetries")
+ .HasColumnType("INTEGER")
+ .HasColumnName("http_max_retries");
+
+ b.Property("HttpTimeout")
+ .HasColumnType("INTEGER")
+ .HasColumnName("http_timeout");
+
+ b.PrimitiveCollection("IgnoredDownloads")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ignored_downloads");
+
+ b.Property("SearchDelay")
+ .HasColumnType("INTEGER")
+ .HasColumnName("search_delay");
+
+ b.Property("SearchEnabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("search_enabled");
+
+ b.ComplexProperty>("Log", "Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig.Log#LoggingConfig", b1 =>
+ {
+ b1.IsRequired();
+
+ b1.Property("ArchiveEnabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("log_archive_enabled");
+
+ b1.Property("ArchiveRetainedCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("log_archive_retained_count");
+
+ b1.Property("ArchiveTimeLimitHours")
+ .HasColumnType("INTEGER")
+ .HasColumnName("log_archive_time_limit_hours");
+
+ b1.Property("Level")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("log_level");
+
+ b1.Property("RetainedFileCount")
+ .HasColumnType("INTEGER")
+ .HasColumnName("log_retained_file_count");
+
+ b1.Property("RollingSizeMB")
+ .HasColumnType("INTEGER")
+ .HasColumnName("log_rolling_size_mb");
+
+ b1.Property("TimeLimitHours")
+ .HasColumnType("INTEGER")
+ .HasColumnName("log_time_limit_hours");
+ });
+
+ b.HasKey("Id")
+ .HasName("pk_general_configs");
+
+ b.ToTable("general_configs", (string)null);
+ });
+
+ modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("id");
+
+ b.Property("CronExpression")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("cron_expression");
+
+ b.Property("DeleteKnownMalware")
+ .HasColumnType("INTEGER")
+ .HasColumnName("delete_known_malware");
+
+ b.Property("DeletePrivate")
+ .HasColumnType("INTEGER")
+ .HasColumnName("delete_private");
+
+ b.Property("Enabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("enabled");
+
+ b.Property("IgnorePrivate")
+ .HasColumnType("INTEGER")
+ .HasColumnName("ignore_private");
+
+ b.PrimitiveCollection("IgnoredDownloads")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("ignored_downloads");
+
+ b.Property("UseAdvancedScheduling")
+ .HasColumnType("INTEGER")
+ .HasColumnName("use_advanced_scheduling");
+
+ b.ComplexProperty>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Lidarr#BlocklistSettings", b1 =>
+ {
+ b1.IsRequired();
+
+ b1.Property("BlocklistPath")
+ .HasColumnType("TEXT")
+ .HasColumnName("lidarr_blocklist_path");
+
+ b1.Property("BlocklistType")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("lidarr_blocklist_type");
+
+ b1.Property("Enabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("lidarr_enabled");
+ });
+
+ b.ComplexProperty>("Radarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Radarr#BlocklistSettings", b1 =>
+ {
+ b1.IsRequired();
+
+ b1.Property("BlocklistPath")
+ .HasColumnType("TEXT")
+ .HasColumnName("radarr_blocklist_path");
+
+ b1.Property("BlocklistType")
+ .IsRequired()
+ .HasColumnType("TEXT")
+ .HasColumnName("radarr_blocklist_type");
+
+ b1.Property("Enabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("radarr_enabled");
+ });
+
+ b.ComplexProperty>("Readarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Readarr#BlocklistSettings", b1 =>
+ {
+ b1.IsRequired();
+
+ b1.Property