From 4ceff127a7379e4b64b3b8a1b87b17472b41d5c3 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Fri, 19 Dec 2025 23:52:59 +0200 Subject: [PATCH] Add option to keep source files when cleaning downloads (#388) --- .../Contracts/Requests/SeedingRuleRequest.cs | 29 + .../UpdateDownloadCleanerConfigRequest.cs | 27 +- .../DownloadCleanerConfigController.cs | 5 +- .../Models/UpdateDownloadCleanerConfigDto.cs | 23 - .../DownloadClient/DelugeServiceDCTests.cs | 46 +- .../DownloadClient/QBitServiceDCTests.cs | 22 +- .../TransmissionServiceDCTests.cs | 20 +- .../DownloadClient/UTorrentServiceDCTests.cs | 46 +- .../Features/Jobs/DownloadCleanerTests.cs | 36 +- .../TestHelpers/TestDataContextFactory.cs | 7 +- .../DownloadClient/Deluge/DelugeClient.cs | 4 +- .../Deluge/DelugeClientWrapper.cs | 4 +- .../DownloadClient/Deluge/DelugeServiceDC.cs | 14 +- .../Deluge/IDelugeClientWrapper.cs | 2 +- .../DownloadClient/DownloadService.cs | 22 +- .../DownloadClient/IDownloadService.cs | 12 +- .../QBittorrent/QBitServiceDC.cs | 12 +- .../Transmission/TransmissionServiceDC.cs | 18 +- .../UTorrent/IUTorrentClientWrapper.cs | 2 +- .../DownloadClient/UTorrent/UTorrentClient.cs | 7 +- .../UTorrent/UTorrentClientWrapper.cs | 4 +- .../UTorrent/UTorrentRequestFactory.cs | 11 + .../UTorrent/UTorrentServiceDC.cs | 14 +- .../Features/Jobs/DownloadCleaner.cs | 2 - .../DownloadCleanerConfigTests.cs | 16 +- ...anCategoryTests.cs => SeedingRuleTests.cs} | 87 +- .../Cleanuparr.Persistence/DataContext.cs | 2 +- ...leteSourceFilesToCleanCategory.Designer.cs | 1095 +++++++++++++++++ ...347_AddDeleteSourceFilesToCleanCategory.cs | 31 + ...nameCleanCategoryToSeedingRule.Designer.cs | 1095 +++++++++++++++++ ...222056_RenameCleanCategoryToSeedingRule.cs | 93 ++ .../Data/DataContextModelSnapshot.cs | 82 +- .../DownloadCleaner/DownloadCleanerConfig.cs | 2 +- .../{CleanCategory.cs => SeedingRule.cs} | 7 +- .../core/services/documentation.service.ts | 1 + .../download-cleaner-settings.component.html | 17 +- .../download-cleaner-settings.component.ts | 1 + .../models/download-cleaner-config.model.ts | 4 +- .../configuration/download-cleaner/index.mdx | 9 + 39 files changed, 2675 insertions(+), 256 deletions(-) create mode 100644 code/backend/Cleanuparr.Api/Features/DownloadCleaner/Contracts/Requests/SeedingRuleRequest.cs delete mode 100644 code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs rename code/backend/Cleanuparr.Persistence.Tests/Models/Configuration/DownloadCleaner/{CleanCategoryTests.cs => SeedingRuleTests.cs} (73%) create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.Designer.cs create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.cs create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.Designer.cs create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.cs rename code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/{CleanCategory.cs => SeedingRule.cs} (87%) 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("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("readarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("readarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("readarr_enabled"); + }); + + b.ComplexProperty>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("sonarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sonarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("sonarr_enabled"); + }); + + b.ComplexProperty>("Whisparr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Whisparr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("whisparr_blocklist_path"); + + b1.Property("BlocklistType") + .HasColumnType("INTEGER") + .HasColumnName("whisparr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("whisparr_enabled"); + }); + + b.HasKey("Id") + .HasName("pk_content_blocker_configs"); + + b.ToTable("content_blocker_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.Property("Tags") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("tags"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_apprise_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_apprise_configs_notification_config_id"); + + b.ToTable("apprise_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("api_key"); + + b.Property("ChannelId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("channel_id"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.HasKey("Id") + .HasName("pk_notifiarr_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_notifiarr_configs_notification_config_id"); + + b.ToTable("notifiarr_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("OnCategoryChanged") + .HasColumnType("INTEGER") + .HasColumnName("on_category_changed"); + + b.Property("OnDownloadCleaned") + .HasColumnType("INTEGER") + .HasColumnName("on_download_cleaned"); + + b.Property("OnFailedImportStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_failed_import_strike"); + + b.Property("OnQueueItemDeleted") + .HasColumnType("INTEGER") + .HasColumnName("on_queue_item_deleted"); + + b.Property("OnSlowStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_slow_strike"); + + b.Property("OnStalledStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_stalled_strike"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_notification_configs"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_notification_configs_name"); + + b.ToTable("notification_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccessToken") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("access_token"); + + b.Property("AuthenticationType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("authentication_type"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.Property("Password") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("priority"); + + b.Property("ServerUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("server_url"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tags"); + + b.PrimitiveCollection("Topics") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("topics"); + + b.Property("Username") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_ntfy_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_ntfy_configs_notification_config_id"); + + b.ToTable("ntfy_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.PushoverConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ApiToken") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("api_token"); + + b.Property("Devices") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("devices"); + + b.Property("Expire") + .HasColumnType("INTEGER") + .HasColumnName("expire"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("priority"); + + b.Property("Retry") + .HasColumnType("INTEGER") + .HasColumnName("retry"); + + b.Property("Sound") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("sound"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tags"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("user_key"); + + b.HasKey("Id") + .HasName("pk_pushover_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_pushover_configs_notification_config_id"); + + b.ToTable("pushover_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("cron_expression"); + + b.Property("DownloadingMetadataMaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("downloading_metadata_max_strikes"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + + b.Property("UseAdvancedScheduling") + .HasColumnType("INTEGER") + .HasColumnName("use_advanced_scheduling"); + + b.ComplexProperty>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 => + { + b1.IsRequired(); + + b1.Property("DeletePrivate") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_delete_private"); + + b1.Property("IgnorePrivate") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_ignore_private"); + + b1.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_max_strikes"); + + b1.Property("PatternMode") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("failed_import_pattern_mode"); + + b1.PrimitiveCollection("Patterns") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("failed_import_patterns"); + + b1.Property("SkipIfNotFoundInClient") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_skip_if_not_found_in_client"); + }); + + b.HasKey("Id") + .HasName("pk_queue_cleaner_configs"); + + b.ToTable("queue_cleaner_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.SlowRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DeletePrivateTorrentsFromClient") + .HasColumnType("INTEGER") + .HasColumnName("delete_private_torrents_from_client"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("IgnoreAboveSize") + .HasColumnType("TEXT") + .HasColumnName("ignore_above_size"); + + b.Property("MaxCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("max_completion_percentage"); + + b.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("max_strikes"); + + b.Property("MaxTimeHours") + .HasColumnType("REAL") + .HasColumnName("max_time_hours"); + + b.Property("MinCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("min_completion_percentage"); + + b.Property("MinSpeed") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("min_speed"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("PrivacyType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("privacy_type"); + + b.Property("QueueCleanerConfigId") + .HasColumnType("TEXT") + .HasColumnName("queue_cleaner_config_id"); + + b.Property("ResetStrikesOnProgress") + .HasColumnType("INTEGER") + .HasColumnName("reset_strikes_on_progress"); + + b.HasKey("Id") + .HasName("pk_slow_rules"); + + b.HasIndex("QueueCleanerConfigId") + .HasDatabaseName("ix_slow_rules_queue_cleaner_config_id"); + + b.ToTable("slow_rules", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.StallRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DeletePrivateTorrentsFromClient") + .HasColumnType("INTEGER") + .HasColumnName("delete_private_torrents_from_client"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("MaxCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("max_completion_percentage"); + + b.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("max_strikes"); + + b.Property("MinCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("min_completion_percentage"); + + b.Property("MinimumProgress") + .HasColumnType("TEXT") + .HasColumnName("minimum_progress"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("PrivacyType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("privacy_type"); + + b.Property("QueueCleanerConfigId") + .HasColumnType("TEXT") + .HasColumnName("queue_cleaner_config_id"); + + b.Property("ResetStrikesOnProgress") + .HasColumnType("INTEGER") + .HasColumnName("reset_strikes_on_progress"); + + b.HasKey("Id") + .HasName("pk_stall_rules"); + + b.HasIndex("QueueCleanerConfigId") + .HasDatabaseName("ix_stall_rules_queue_cleaner_config_id"); + + b.ToTable("stall_rules", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.State.BlacklistSyncHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DownloadClientId") + .HasColumnType("TEXT") + .HasColumnName("download_client_id"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hash"); + + b.HasKey("Id") + .HasName("pk_blacklist_sync_history"); + + b.HasIndex("DownloadClientId") + .HasDatabaseName("ix_blacklist_sync_history_download_client_id"); + + b.HasIndex("Hash") + .HasDatabaseName("ix_blacklist_sync_history_hash"); + + b.HasIndex("Hash", "DownloadClientId") + .IsUnique() + .HasDatabaseName("ix_blacklist_sync_history_hash_download_client_id"); + + b.ToTable("blacklist_sync_history", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", "ArrConfig") + .WithMany("Instances") + .HasForeignKey("ArrConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_arr_instances_arr_configs_arr_config_id"); + + b.Navigation("ArrConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig") + .WithMany("Categories") + .HasForeignKey("DownloadCleanerConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clean_categories_download_cleaner_configs_download_cleaner_config_id"); + + b.Navigation("DownloadCleanerConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("AppriseConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_apprise_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("NotifiarrConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notifiarr_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("NtfyConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_ntfy_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.PushoverConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("PushoverConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.PushoverConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_pushover_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.SlowRule", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", "QueueCleanerConfig") + .WithMany("SlowRules") + .HasForeignKey("QueueCleanerConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_slow_rules_queue_cleaner_configs_queue_cleaner_config_id"); + + b.Navigation("QueueCleanerConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.StallRule", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", "QueueCleanerConfig") + .WithMany("StallRules") + .HasForeignKey("QueueCleanerConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_stall_rules_queue_cleaner_configs_queue_cleaner_config_id"); + + b.Navigation("QueueCleanerConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.State.BlacklistSyncHistory", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", "DownloadClient") + .WithMany() + .HasForeignKey("DownloadClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_blacklist_sync_history_download_clients_download_client_id"); + + b.Navigation("DownloadClient"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b => + { + b.Navigation("Instances"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b => + { + b.Navigation("AppriseConfiguration"); + + b.Navigation("NotifiarrConfiguration"); + + b.Navigation("NtfyConfiguration"); + + b.Navigation("PushoverConfiguration"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b => + { + b.Navigation("SlowRules"); + + b.Navigation("StallRules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.cs new file mode 100644 index 00000000..cc7fdbde --- /dev/null +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216204347_AddDeleteSourceFilesToCleanCategory.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Cleanuparr.Persistence.Migrations.Data +{ + /// + public partial class AddDeleteSourceFilesToCleanCategory : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "delete_source_files", + table: "clean_categories", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.Sql("UPDATE clean_categories SET delete_source_files = 1"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "delete_source_files", + table: "clean_categories"); + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.Designer.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.Designer.cs new file mode 100644 index 00000000..9b9e864d --- /dev/null +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.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("20251216222056_RenameCleanCategoryToSeedingRule")] + partial class RenameCleanCategoryToSeedingRule + { + /// + 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.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.DownloadCleaner.SeedingRule", 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_seeding_rules"); + + b.HasIndex("DownloadCleanerConfigId") + .HasDatabaseName("ix_seeding_rules_download_cleaner_config_id"); + + b.ToTable("seeding_rules", (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("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("readarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("readarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("readarr_enabled"); + }); + + b.ComplexProperty>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("sonarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sonarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("sonarr_enabled"); + }); + + b.ComplexProperty>("Whisparr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Whisparr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("whisparr_blocklist_path"); + + b1.Property("BlocklistType") + .HasColumnType("INTEGER") + .HasColumnName("whisparr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("whisparr_enabled"); + }); + + b.HasKey("Id") + .HasName("pk_content_blocker_configs"); + + b.ToTable("content_blocker_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.Property("Tags") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("tags"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_apprise_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_apprise_configs_notification_config_id"); + + b.ToTable("apprise_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("api_key"); + + b.Property("ChannelId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("channel_id"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.HasKey("Id") + .HasName("pk_notifiarr_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_notifiarr_configs_notification_config_id"); + + b.ToTable("notifiarr_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("OnCategoryChanged") + .HasColumnType("INTEGER") + .HasColumnName("on_category_changed"); + + b.Property("OnDownloadCleaned") + .HasColumnType("INTEGER") + .HasColumnName("on_download_cleaned"); + + b.Property("OnFailedImportStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_failed_import_strike"); + + b.Property("OnQueueItemDeleted") + .HasColumnType("INTEGER") + .HasColumnName("on_queue_item_deleted"); + + b.Property("OnSlowStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_slow_strike"); + + b.Property("OnStalledStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_stalled_strike"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_notification_configs"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_notification_configs_name"); + + b.ToTable("notification_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("AccessToken") + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("access_token"); + + b.Property("AuthenticationType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("authentication_type"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.Property("Password") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("priority"); + + b.Property("ServerUrl") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("server_url"); + + b.PrimitiveCollection("Tags") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tags"); + + b.PrimitiveCollection("Topics") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("topics"); + + b.Property("Username") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_ntfy_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_ntfy_configs_notification_config_id"); + + b.ToTable("ntfy_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.PushoverConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ApiToken") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("api_token"); + + b.Property("Devices") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("devices"); + + b.Property("Expire") + .HasColumnType("INTEGER") + .HasColumnName("expire"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("priority"); + + b.Property("Retry") + .HasColumnType("INTEGER") + .HasColumnName("retry"); + + b.Property("Sound") + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("sound"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tags"); + + b.Property("UserKey") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("user_key"); + + b.HasKey("Id") + .HasName("pk_pushover_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_pushover_configs_notification_config_id"); + + b.ToTable("pushover_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("cron_expression"); + + b.Property("DownloadingMetadataMaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("downloading_metadata_max_strikes"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + + b.Property("UseAdvancedScheduling") + .HasColumnType("INTEGER") + .HasColumnName("use_advanced_scheduling"); + + b.ComplexProperty>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 => + { + b1.IsRequired(); + + b1.Property("DeletePrivate") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_delete_private"); + + b1.Property("IgnorePrivate") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_ignore_private"); + + b1.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_max_strikes"); + + b1.Property("PatternMode") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("failed_import_pattern_mode"); + + b1.PrimitiveCollection("Patterns") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("failed_import_patterns"); + + b1.Property("SkipIfNotFoundInClient") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_skip_if_not_found_in_client"); + }); + + b.HasKey("Id") + .HasName("pk_queue_cleaner_configs"); + + b.ToTable("queue_cleaner_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.SlowRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DeletePrivateTorrentsFromClient") + .HasColumnType("INTEGER") + .HasColumnName("delete_private_torrents_from_client"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("IgnoreAboveSize") + .HasColumnType("TEXT") + .HasColumnName("ignore_above_size"); + + b.Property("MaxCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("max_completion_percentage"); + + b.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("max_strikes"); + + b.Property("MaxTimeHours") + .HasColumnType("REAL") + .HasColumnName("max_time_hours"); + + b.Property("MinCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("min_completion_percentage"); + + b.Property("MinSpeed") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("min_speed"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("PrivacyType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("privacy_type"); + + b.Property("QueueCleanerConfigId") + .HasColumnType("TEXT") + .HasColumnName("queue_cleaner_config_id"); + + b.Property("ResetStrikesOnProgress") + .HasColumnType("INTEGER") + .HasColumnName("reset_strikes_on_progress"); + + b.HasKey("Id") + .HasName("pk_slow_rules"); + + b.HasIndex("QueueCleanerConfigId") + .HasDatabaseName("ix_slow_rules_queue_cleaner_config_id"); + + b.ToTable("slow_rules", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.StallRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DeletePrivateTorrentsFromClient") + .HasColumnType("INTEGER") + .HasColumnName("delete_private_torrents_from_client"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("MaxCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("max_completion_percentage"); + + b.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("max_strikes"); + + b.Property("MinCompletionPercentage") + .HasColumnType("INTEGER") + .HasColumnName("min_completion_percentage"); + + b.Property("MinimumProgress") + .HasColumnType("TEXT") + .HasColumnName("minimum_progress"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("PrivacyType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("privacy_type"); + + b.Property("QueueCleanerConfigId") + .HasColumnType("TEXT") + .HasColumnName("queue_cleaner_config_id"); + + b.Property("ResetStrikesOnProgress") + .HasColumnType("INTEGER") + .HasColumnName("reset_strikes_on_progress"); + + b.HasKey("Id") + .HasName("pk_stall_rules"); + + b.HasIndex("QueueCleanerConfigId") + .HasDatabaseName("ix_stall_rules_queue_cleaner_config_id"); + + b.ToTable("stall_rules", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.State.BlacklistSyncHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DownloadClientId") + .HasColumnType("TEXT") + .HasColumnName("download_client_id"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hash"); + + b.HasKey("Id") + .HasName("pk_blacklist_sync_history"); + + b.HasIndex("DownloadClientId") + .HasDatabaseName("ix_blacklist_sync_history_download_client_id"); + + b.HasIndex("Hash") + .HasDatabaseName("ix_blacklist_sync_history_hash"); + + b.HasIndex("Hash", "DownloadClientId") + .IsUnique() + .HasDatabaseName("ix_blacklist_sync_history_hash_download_client_id"); + + b.ToTable("blacklist_sync_history", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", "ArrConfig") + .WithMany("Instances") + .HasForeignKey("ArrConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_arr_instances_arr_configs_arr_config_id"); + + b.Navigation("ArrConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.SeedingRule", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig") + .WithMany("Categories") + .HasForeignKey("DownloadCleanerConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_seeding_rules_download_cleaner_configs_download_cleaner_config_id"); + + b.Navigation("DownloadCleanerConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("AppriseConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_apprise_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("NotifiarrConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notifiarr_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("NtfyConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NtfyConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_ntfy_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.PushoverConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("PushoverConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.PushoverConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_pushover_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.SlowRule", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", "QueueCleanerConfig") + .WithMany("SlowRules") + .HasForeignKey("QueueCleanerConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_slow_rules_queue_cleaner_configs_queue_cleaner_config_id"); + + b.Navigation("QueueCleanerConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.StallRule", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", "QueueCleanerConfig") + .WithMany("StallRules") + .HasForeignKey("QueueCleanerConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_stall_rules_queue_cleaner_configs_queue_cleaner_config_id"); + + b.Navigation("QueueCleanerConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.State.BlacklistSyncHistory", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", "DownloadClient") + .WithMany() + .HasForeignKey("DownloadClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_blacklist_sync_history_download_clients_download_client_id"); + + b.Navigation("DownloadClient"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b => + { + b.Navigation("Instances"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b => + { + b.Navigation("AppriseConfiguration"); + + b.Navigation("NotifiarrConfiguration"); + + b.Navigation("NtfyConfiguration"); + + b.Navigation("PushoverConfiguration"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b => + { + b.Navigation("SlowRules"); + + b.Navigation("StallRules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.cs new file mode 100644 index 00000000..64486568 --- /dev/null +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20251216222056_RenameCleanCategoryToSeedingRule.cs @@ -0,0 +1,93 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Cleanuparr.Persistence.Migrations.Data +{ + /// + public partial class RenameCleanCategoryToSeedingRule : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "seeding_rules", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + download_cleaner_config_id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", nullable: false), + max_ratio = table.Column(type: "REAL", nullable: false), + min_seed_time = table.Column(type: "REAL", nullable: false), + max_seed_time = table.Column(type: "REAL", nullable: false), + delete_source_files = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_seeding_rules", x => x.id); + table.ForeignKey( + name: "fk_seeding_rules_download_cleaner_configs_download_cleaner_config_id", + column: x => x.download_cleaner_config_id, + principalTable: "download_cleaner_configs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_seeding_rules_download_cleaner_config_id", + table: "seeding_rules", + column: "download_cleaner_config_id"); + + migrationBuilder.Sql(@" + INSERT INTO seeding_rules (id, download_cleaner_config_id, name, max_ratio, min_seed_time, max_seed_time, delete_source_files) + SELECT id, download_cleaner_config_id, name, max_ratio, min_seed_time, max_seed_time, delete_source_files + FROM clean_categories; + "); + + migrationBuilder.DropTable( + name: "clean_categories"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "clean_categories", + columns: table => new + { + id = table.Column(type: "TEXT", nullable: false), + download_cleaner_config_id = table.Column(type: "TEXT", nullable: false), + name = table.Column(type: "TEXT", nullable: false), + max_ratio = table.Column(type: "REAL", nullable: false), + min_seed_time = table.Column(type: "REAL", nullable: false), + max_seed_time = table.Column(type: "REAL", nullable: false), + delete_source_files = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_clean_categories", x => x.id); + table.ForeignKey( + name: "fk_clean_categories_download_cleaner_configs_download_cleaner_config_id", + column: x => x.download_cleaner_config_id, + principalTable: "download_cleaner_configs", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_clean_categories_download_cleaner_config_id", + table: "clean_categories", + column: "download_cleaner_config_id"); + + migrationBuilder.Sql(@" + INSERT INTO clean_categories (id, download_cleaner_config_id, name, max_ratio, min_seed_time, max_seed_time, delete_source_files) + SELECT id, download_cleaner_config_id, name, max_ratio, min_seed_time, max_seed_time, delete_source_files + FROM seeding_rules; + "); + + migrationBuilder.DropTable( + name: "seeding_rules"); + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs index fdcdd7f3..e835242e 100644 --- a/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs @@ -105,43 +105,6 @@ namespace Cleanuparr.Persistence.Migrations.Data 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("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") @@ -200,6 +163,47 @@ namespace Cleanuparr.Persistence.Migrations.Data b.ToTable("download_cleaner_configs", (string)null); }); + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.SeedingRule", 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_seeding_rules"); + + b.HasIndex("DownloadCleanerConfigId") + .HasDatabaseName("ix_seeding_rules_download_cleaner_config_id"); + + b.ToTable("seeding_rules", (string)null); + }); + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", b => { b.Property("Id") @@ -959,14 +963,14 @@ namespace Cleanuparr.Persistence.Migrations.Data b.Navigation("ArrConfig"); }); - modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b => + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.SeedingRule", b => { b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig") .WithMany("Categories") .HasForeignKey("DownloadCleanerConfigId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() - .HasConstraintName("fk_clean_categories_download_cleaner_configs_download_cleaner_config_id"); + .HasConstraintName("fk_seeding_rules_download_cleaner_configs_download_cleaner_config_id"); b.Navigation("DownloadCleanerConfig"); }); diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs index 987996f7..cc1cdaff 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs @@ -19,7 +19,7 @@ public sealed record DownloadCleanerConfig : IJobConfig /// public bool UseAdvancedScheduling { get; set; } - public List Categories { get; set; } = []; + public List Categories { get; set; } = []; public bool DeletePrivate { get; set; } diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/CleanCategory.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/SeedingRule.cs similarity index 87% rename from code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/CleanCategory.cs rename to code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/SeedingRule.cs index 006b2579..d6db3852 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/CleanCategory.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/SeedingRule.cs @@ -4,7 +4,7 @@ using ValidationException = Cleanuparr.Domain.Exceptions.ValidationException; namespace Cleanuparr.Persistence.Models.Configuration.DownloadCleaner; -public sealed record CleanCategory : IConfig +public sealed record SeedingRule : IConfig { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -31,6 +31,11 @@ public sealed record CleanCategory : IConfig /// public required double MaxSeedTime { get; init; } = -1; + /// + /// Whether to delete the source files when cleaning the download. + /// + public required bool DeleteSourceFiles { get; init; } + public void Validate() { if (string.IsNullOrEmpty(Name.Trim())) diff --git a/code/frontend/src/app/core/services/documentation.service.ts b/code/frontend/src/app/core/services/documentation.service.ts index a2e47a31..85041309 100644 --- a/code/frontend/src/app/core/services/documentation.service.ts +++ b/code/frontend/src/app/core/services/documentation.service.ts @@ -72,6 +72,7 @@ export class DocumentationService { 'maxRatio': 'max-ratio', 'minSeedTime': 'min-seed-time', 'maxSeedTime': 'max-seed-time', + 'deleteSourceFiles': 'delete-source-files', 'unlinkedEnabled': 'enable-unlinked-download-handling', 'unlinkedTargetCategory': 'target-category', 'unlinkedUseTag': 'use-tag', diff --git a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html index bae9d13a..f8eb2a41 100644 --- a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html +++ b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html @@ -228,8 +228,8 @@
@@ -243,6 +243,19 @@ Maximum time to seed before removing (-1 means disabled)
+ +
+ +
+ + When enabled, the source files will be deleted when the download is removed +
+
diff --git a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts index c07f78f3..a051df7c 100644 --- a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts +++ b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts @@ -213,6 +213,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent maxRatio: [category.maxRatio, [Validators.min(-1), Validators.required]], minSeedTime: [category.minSeedTime, [Validators.min(0), Validators.required]], maxSeedTime: [category.maxSeedTime, [Validators.min(-1), Validators.required]], + deleteSourceFiles: [category.deleteSourceFiles], }, { validators: this.validateCategory }); } diff --git a/code/frontend/src/app/shared/models/download-cleaner-config.model.ts b/code/frontend/src/app/shared/models/download-cleaner-config.model.ts index 71317ea3..0408cc14 100644 --- a/code/frontend/src/app/shared/models/download-cleaner-config.model.ts +++ b/code/frontend/src/app/shared/models/download-cleaner-config.model.ts @@ -22,6 +22,7 @@ export interface CleanCategory { maxRatio: number; minSeedTime: number; // hours maxSeedTime: number; // hours + deleteSourceFiles: boolean; } export interface JobSchedule { @@ -35,7 +36,8 @@ export function createDefaultCategory(): CleanCategory { name: '', maxRatio: -1, // -1 means disabled minSeedTime: 0, - maxSeedTime: -1 // -1 means disabled + maxSeedTime: -1, // -1 means disabled + deleteSourceFiles: true }; } diff --git a/docs/docs/configuration/download-cleaner/index.mdx b/docs/docs/configuration/download-cleaner/index.mdx index 8c3344da..c8d29b51 100644 --- a/docs/docs/configuration/download-cleaner/index.mdx +++ b/docs/docs/configuration/download-cleaner/index.mdx @@ -165,6 +165,15 @@ Maximum time in hours to seed before removing a download regardless of ratio. Se + + +When enabled, the source files will be deleted from disk when the download is removed from the download client. When disabled, only the torrent entry is removed while preserving the underlying files. + + +