mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-23 22:18:39 -05:00
Add option to keep source files when cleaning downloads (#388)
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Max ratio before removing a download.
|
||||
/// </summary>
|
||||
public double MaxRatio { get; init; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Min number of hours to seed before removing a download, if the ratio has been met.
|
||||
/// </summary>
|
||||
public double MinSeedTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of hours to seed before removing a download.
|
||||
/// </summary>
|
||||
public double MaxSeedTime { get; init; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to delete the source files when cleaning the download.
|
||||
/// </summary>
|
||||
public bool DeleteSourceFiles { get; init; } = true;
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public bool UseAdvancedScheduling { get; init; }
|
||||
|
||||
public List<CleanCategoryRequest> Categories { get; init; } = [];
|
||||
public List<SeedingRuleRequest> Categories { get; init; } = [];
|
||||
|
||||
public bool DeletePrivate { get; init; }
|
||||
|
||||
@@ -32,24 +30,3 @@ public record UpdateDownloadCleanerConfigRequest
|
||||
|
||||
public List<string> IgnoredDownloads { get; init; } = [];
|
||||
}
|
||||
|
||||
public record CleanCategoryRequest
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Max ratio before removing a download.
|
||||
/// </summary>
|
||||
public double MaxRatio { get; init; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Min number of hours to seed before removing a download, if the ratio has been met.
|
||||
/// </summary>
|
||||
public double MinSeedTime { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of hours to seed before removing a download.
|
||||
/// </summary>
|
||||
public double MaxSeedTime { get; init; } = -1;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests;
|
||||
|
||||
namespace Cleanuparr.Api.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy namespace shim; prefer <see cref="UpdateDownloadCleanerConfigRequest"/> from
|
||||
/// <c>Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests</c>.
|
||||
/// </summary>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy namespace shim; prefer <see cref="CleanCategoryRequest"/> from
|
||||
/// <c>Cleanuparr.Api.Features.DownloadCleaner.Contracts.Requests</c>.
|
||||
/// </summary>
|
||||
[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;
|
||||
@@ -133,10 +133,10 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
new DelugeItemWrapper(new DownloadStatus { Hash = "hash3", Label = "music", Trackers = new List<Tracker>(), DownloadLocation = "/downloads" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<DelugeServiceFixture>
|
||||
new DelugeItemWrapper(new DownloadStatus { Hash = "hash1", Label = "Movies", Trackers = new List<Tracker>(), DownloadLocation = "/downloads" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<DelugeServiceFixture>
|
||||
new DelugeItemWrapper(new DownloadStatus { Hash = "hash1", Label = "music", Trackers = new List<Tracker>(), DownloadLocation = "/downloads" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<DelugeServiceFixture>
|
||||
const string hash = "TEST-HASH";
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("test-hash"))))
|
||||
.Setup(x => x.DeleteTorrents(It.Is<List<string>>(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<List<string>>(h => h.Contains("test-hash"))),
|
||||
x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("test-hash")), true),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -362,15 +362,35 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
const string hash = "UPPERCASE-HASH";
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.DeleteTorrents(It.IsAny<List<string>>()))
|
||||
.Setup(x => x.DeleteTorrents(It.IsAny<List<string>>(), true))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash);
|
||||
await sut.DeleteDownload(hash, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("uppercase-hash"))),
|
||||
x => x.DeleteTorrents(It.Is<List<string>>(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<List<string>>(h => h.Contains("test-hash")), false))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash, false);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.DeleteTorrents(It.Is<List<string>>(h => h.Contains("test-hash")), false),
|
||||
Times.Once);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,10 +214,10 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
new QBitItemWrapper(new TorrentInfo { Hash = "hash3", Category = "music" }, Array.Empty<TorrentTracker>(), false)
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<QBitServiceFixture>
|
||||
new QBitItemWrapper(new TorrentInfo { Hash = "hash1", Category = "Movies" }, Array.Empty<TorrentTracker>(), false)
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<QBitServiceFixture>
|
||||
new QBitItemWrapper(new TorrentInfo { Hash = "hash1", Category = "movies" }, Array.Empty<TorrentTracker>(), false)
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<QBitServiceFixture>
|
||||
new QBitItemWrapper(new TorrentInfo { Hash = "hash1", Category = "music" }, Array.Empty<TorrentTracker>(), false)
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<QBitServiceFixture>
|
||||
.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<QBitServiceFixture>
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash);
|
||||
await sut.DeleteDownload(hash, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
|
||||
@@ -138,10 +138,10 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
new TransmissionItemWrapper(new TorrentInfo { HashString = "hash3", DownloadDir = "/downloads/music" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<TransmissionServiceFixtu
|
||||
new TransmissionItemWrapper(new TorrentInfo { HashString = "hash1", DownloadDir = "/downloads/Movies" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<TransmissionServiceFixtu
|
||||
new TransmissionItemWrapper(new TorrentInfo { HashString = "hash1", DownloadDir = "/downloads/music" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<TransmissionServiceFixtu
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash);
|
||||
await sut.DeleteDownload(hash, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
@@ -379,7 +379,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
.ReturnsAsync((TransmissionTorrents?)null);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash);
|
||||
await sut.DeleteDownload(hash, true);
|
||||
|
||||
// Assert - no exception thrown
|
||||
_fixture.ClientWrapper.Verify(
|
||||
@@ -426,7 +426,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
.ReturnsAsync(torrents);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash);
|
||||
await sut.DeleteDownload(hash, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
|
||||
@@ -126,10 +126,10 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
new UTorrentItemWrapper(new UTorrentItem { Hash = "hash3", Label = "music" }, new UTorrentProperties { Hash = "hash3", Pex = 1, Trackers = "" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<UTorrentServiceFixture>
|
||||
new UTorrentItemWrapper(new UTorrentItem { Hash = "hash1", Label = "Movies" }, new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<UTorrentServiceFixture>
|
||||
new UTorrentItemWrapper(new UTorrentItem { Hash = "hash1", Label = "music" }, new UTorrentProperties { Hash = "hash1", Pex = 1, Trackers = "" })
|
||||
};
|
||||
|
||||
var categories = new List<CleanCategory>
|
||||
var categories = new List<SeedingRule>
|
||||
{
|
||||
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<UTorrentServiceFixture>
|
||||
const string hash = "TEST-HASH";
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("test-hash"))))
|
||||
.Setup(x => x.RemoveTorrentsAsync(It.Is<List<string>>(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<List<string>>(h => h.Contains("test-hash"))),
|
||||
x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("test-hash")), true),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
@@ -312,15 +312,35 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
const string hash = "UPPERCASE-HASH";
|
||||
|
||||
_fixture.ClientWrapper
|
||||
.Setup(x => x.RemoveTorrentsAsync(It.IsAny<List<string>>()))
|
||||
.Setup(x => x.RemoveTorrentsAsync(It.IsAny<List<string>>(), true))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash);
|
||||
await sut.DeleteDownload(hash, true);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("uppercase-hash"))),
|
||||
x => x.RemoveTorrentsAsync(It.Is<List<string>>(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<List<string>>(h => h.Contains("test-hash")), false))
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
// Act
|
||||
await sut.DeleteDownload(hash, false);
|
||||
|
||||
// Assert
|
||||
_fixture.ClientWrapper.Verify(
|
||||
x => x.RemoveTorrentsAsync(It.Is<List<string>>(h => h.Contains("test-hash")), false),
|
||||
Times.Once);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ITorrentItemWrapper>();
|
||||
@@ -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<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.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<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns("test-hash");
|
||||
@@ -434,13 +434,13 @@ public class DownloadCleanerTests : IDisposable
|
||||
mockDownloadService
|
||||
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
|
||||
It.IsAny<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.Returns([mockTorrent.Object]);
|
||||
mockDownloadService
|
||||
.Setup(x => x.CleanDownloadsAsync(
|
||||
It.IsAny<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.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<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.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<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns("test-hash");
|
||||
@@ -769,7 +769,7 @@ public class DownloadCleanerTests : IDisposable
|
||||
mockDownloadService
|
||||
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
|
||||
It.IsAny<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.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<ITorrentItemWrapper>();
|
||||
mockTorrent.Setup(x => x.Hash).Returns("test-hash");
|
||||
@@ -815,13 +815,13 @@ public class DownloadCleanerTests : IDisposable
|
||||
mockDownloadService
|
||||
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
|
||||
It.IsAny<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.Returns([mockTorrent.Object]);
|
||||
mockDownloadService
|
||||
.Setup(x => x.CleanDownloadsAsync(
|
||||
It.IsAny<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.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<ITorrentItemWrapper>();
|
||||
@@ -868,7 +868,7 @@ public class DownloadCleanerTests : IDisposable
|
||||
mockDownloadService
|
||||
.Setup(x => x.FilterDownloadsToBeCleanedAsync(
|
||||
It.IsAny<List<ITorrentItemWrapper>>(),
|
||||
It.IsAny<List<CleanCategory>>()
|
||||
It.IsAny<List<SeedingRule>>()
|
||||
))
|
||||
.Returns([]);
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ public static class TestDataContextFactory
|
||||
/// <summary>
|
||||
/// Adds a clean category to the download cleaner config
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@@ -156,9 +156,9 @@ public sealed class DelugeClient
|
||||
await SendRequest<DelugeResponse<object>>("core.set_torrent_options", hash, filePriorities);
|
||||
}
|
||||
|
||||
public async Task DeleteTorrents(List<string> hashes)
|
||||
public async Task DeleteTorrents(List<string> hashes, bool removeData)
|
||||
{
|
||||
await SendRequest<DelugeResponse<object>>("core.remove_torrents", hashes, true);
|
||||
await SendRequest<DelugeResponse<object>>("core.remove_torrents", hashes, removeData);
|
||||
}
|
||||
|
||||
private async Task<String> PostJson(String json)
|
||||
|
||||
@@ -35,8 +35,8 @@ public sealed class DelugeClientWrapper : IDelugeClientWrapper
|
||||
public Task<List<DownloadStatus>?> GetStatusForAllTorrents()
|
||||
=> _client.GetStatusForAllTorrents();
|
||||
|
||||
public Task DeleteTorrents(List<string> hashes)
|
||||
=> _client.DeleteTorrents(hashes);
|
||||
public Task DeleteTorrents(List<string> hashes, bool removeData)
|
||||
=> _client.DeleteTorrents(hashes, removeData);
|
||||
|
||||
public Task ChangeFilesPriority(string hash, List<int> priorities)
|
||||
=> _client.ChangeFilesPriority(hash, priorities);
|
||||
|
||||
@@ -25,9 +25,9 @@ public partial class DelugeService
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categories) =>
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> 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<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, List<string> categories) =>
|
||||
@@ -37,9 +37,9 @@ public partial class DelugeService
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface IDelugeClientWrapper
|
||||
Task<DelugeTorrent?> GetTorrent(string hash);
|
||||
Task<DelugeTorrentExtended?> GetTorrentExtended(string hash);
|
||||
Task<List<DownloadStatus>?> GetStatusForAllTorrents();
|
||||
Task DeleteTorrents(List<string> hashes);
|
||||
Task DeleteTorrents(List<string> hashes, bool removeData);
|
||||
Task ChangeFilesPriority(string hash, List<int> priorities);
|
||||
Task<IReadOnlyList<string>> GetLabels();
|
||||
Task CreateLabel(string label);
|
||||
|
||||
@@ -82,19 +82,19 @@ public abstract class DownloadService : IDownloadService
|
||||
public abstract Task<DownloadCheckResult> ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task DeleteDownload(string hash);
|
||||
public abstract Task DeleteDownload(string hash, bool deleteSourceFiles);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task<List<ITorrentItemWrapper>> GetSeedingDownloads();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categories);
|
||||
public abstract List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> seedingRules);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract List<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, List<string> categories);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task CleanDownloadsAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categoriesToClean)
|
||||
public virtual async Task CleanDownloadsAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> 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.
|
||||
/// </summary>
|
||||
/// <param name="torrent">The torrent to delete</param>
|
||||
protected abstract Task DeleteDownloadInternal(ITorrentItemWrapper torrent);
|
||||
/// <param name="deleteSourceFiles">Whether to delete the source files along with the torrent</param>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -36,9 +36,9 @@ public interface IDownloadService : IDisposable
|
||||
/// Filters downloads that should be cleaned.
|
||||
/// </summary>
|
||||
/// <param name="downloads">The downloads to filter.</param>
|
||||
/// <param name="categories">The categories by which to filter the downloads.</param>
|
||||
/// <param name="seedingRules">The seeding rules by which to filter the downloads.</param>
|
||||
/// <returns>A list of downloads for the provided categories.</returns>
|
||||
List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categories);
|
||||
List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> seedingRules);
|
||||
|
||||
/// <summary>
|
||||
/// Filters downloads that should have their category changed.
|
||||
@@ -52,8 +52,8 @@ public interface IDownloadService : IDisposable
|
||||
/// Cleans the downloads.
|
||||
/// </summary>
|
||||
/// <param name="downloads">The downloads to clean.</param>
|
||||
/// <param name="categoriesToClean">The categories that should be cleaned.</param>
|
||||
Task CleanDownloadsAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categoriesToClean);
|
||||
/// <param name="seedingRules">The seeding rules.</param>
|
||||
Task CleanDownloadsAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> seedingRules);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the category for downloads that have no hardlinks.
|
||||
@@ -64,7 +64,9 @@ public interface IDownloadService : IDisposable
|
||||
/// <summary>
|
||||
/// Deletes a download item.
|
||||
/// </summary>
|
||||
public Task DeleteDownload(string hash);
|
||||
/// <param name="hash">The torrent hash.</param>
|
||||
/// <param name="deleteSourceFiles">Whether to delete the source files along with the torrent. Defaults to true.</param>
|
||||
public Task DeleteDownload(string hash, bool deleteSourceFiles);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a category.
|
||||
|
||||
@@ -33,10 +33,10 @@ public partial class QBitService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categories) =>
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> 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();
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -61,9 +61,9 @@ public partial class QBitService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
|
||||
@@ -21,10 +21,10 @@ public partial class TransmissionService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categories)
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> 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
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -13,5 +13,5 @@ public interface IUTorrentClientWrapper
|
||||
Task<List<string>> GetLabelsAsync();
|
||||
Task SetTorrentLabelAsync(string hash, string label);
|
||||
Task SetFilesPriorityAsync(string hash, List<int> fileIndexes, int priority);
|
||||
Task RemoveTorrentsAsync(List<string> hashes);
|
||||
Task RemoveTorrentsAsync(List<string> hashes, bool deleteData);
|
||||
}
|
||||
|
||||
@@ -210,13 +210,16 @@ public sealed class UTorrentClient
|
||||
/// Removes torrents from µTorrent
|
||||
/// </summary>
|
||||
/// <param name="hashes">List of torrent hashes to remove</param>
|
||||
public async Task RemoveTorrentsAsync(List<string> hashes)
|
||||
/// <param name="deleteData">Whether to delete the downloaded data files</param>
|
||||
public async Task RemoveTorrentsAsync(List<string> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,6 @@ public sealed class UTorrentClientWrapper : IUTorrentClientWrapper
|
||||
public Task SetFilesPriorityAsync(string hash, List<int> fileIndexes, int priority)
|
||||
=> _client.SetFilesPriorityAsync(hash, fileIndexes, priority);
|
||||
|
||||
public Task RemoveTorrentsAsync(List<string> hashes)
|
||||
=> _client.RemoveTorrentsAsync(hashes);
|
||||
public Task RemoveTorrentsAsync(List<string> hashes, bool deleteData)
|
||||
=> _client.RemoveTorrentsAsync(hashes, deleteData);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,17 @@ public static class UTorrentRequestFactory
|
||||
.WithParameter("hash", hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a request to remove a torrent without deleting its data
|
||||
/// </summary>
|
||||
/// <param name="hash">Torrent hash</param>
|
||||
/// <returns>Request for remove torrent API call</returns>
|
||||
public static UTorrentRequest CreateRemoveTorrentRequest(string hash)
|
||||
{
|
||||
return UTorrentRequest.Create("action=removetorrent", string.Empty)
|
||||
.WithParameter("hash", hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a request to set file priorities for a torrent
|
||||
/// </summary>
|
||||
|
||||
@@ -24,9 +24,9 @@ public partial class UTorrentService
|
||||
return result;
|
||||
}
|
||||
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<CleanCategory> categories) =>
|
||||
public override List<ITorrentItemWrapper>? FilterDownloadsToBeCleanedAsync(List<ITorrentItemWrapper>? downloads, List<SeedingRule> 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<ITorrentItemWrapper>? FilterDownloadsToChangeCategoryAsync(List<ITorrentItemWrapper>? downloads, List<string> categories) =>
|
||||
@@ -36,9 +36,9 @@ public partial class UTorrentService
|
||||
.ToList();
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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)
|
||||
|
||||
@@ -128,8 +128,6 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
|
||||
await ChangeUnlinkedCategoriesAsync(isUnlinkedEnabled, downloadServiceToDownloadsMap, config);
|
||||
await CleanDownloadsAsync(downloadServiceToDownloadsMap, config);
|
||||
|
||||
|
||||
|
||||
foreach (var downloadService in downloadServices)
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ValidationException>(() => 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<ValidationException>(() => 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<ValidationException>(() => 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<ValidationException>(() => 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<ValidationException>(() => 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<ValidationException>(() => 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<ValidationException>(() => 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();
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class DataContext : DbContext
|
||||
|
||||
public DbSet<DownloadCleanerConfig> DownloadCleanerConfigs { get; set; }
|
||||
|
||||
public DbSet<CleanCategory> CleanCategories { get; set; }
|
||||
public DbSet<SeedingRule> SeedingRules { get; set; }
|
||||
|
||||
public DbSet<ArrConfig> ArrConfigs { get; set; }
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDeleteSourceFilesToCleanCategory : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "delete_source_files",
|
||||
table: "clean_categories",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.Sql("UPDATE clean_categories SET delete_source_files = 1");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "delete_source_files",
|
||||
table: "clean_categories");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenameCleanCategoryToSeedingRule : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "seeding_rules",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
download_cleaner_config_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
max_ratio = table.Column<double>(type: "REAL", nullable: false),
|
||||
min_seed_time = table.Column<double>(type: "REAL", nullable: false),
|
||||
max_seed_time = table.Column<double>(type: "REAL", nullable: false),
|
||||
delete_source_files = table.Column<bool>(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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "clean_categories",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
download_cleaner_config_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
name = table.Column<string>(type: "TEXT", nullable: false),
|
||||
max_ratio = table.Column<double>(type: "REAL", nullable: false),
|
||||
min_seed_time = table.Column<double>(type: "REAL", nullable: false),
|
||||
max_seed_time = table.Column<double>(type: "REAL", nullable: false),
|
||||
delete_source_files = table.Column<bool>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("DownloadCleanerConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_cleaner_config_id");
|
||||
|
||||
b.Property<double>("MaxRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_ratio");
|
||||
|
||||
b.Property<double>("MaxSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_seed_time");
|
||||
|
||||
b.Property<double>("MinSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("min_seed_time");
|
||||
|
||||
b.Property<string>("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<Guid>("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<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("DeleteSourceFiles")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_source_files");
|
||||
|
||||
b.Property<Guid>("DownloadCleanerConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_cleaner_config_id");
|
||||
|
||||
b.Property<double>("MaxRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_ratio");
|
||||
|
||||
b.Property<double>("MaxSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_seed_time");
|
||||
|
||||
b.Property<double>("MinSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("min_seed_time");
|
||||
|
||||
b.Property<string>("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<Guid>("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");
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ public sealed record DownloadCleanerConfig : IJobConfig
|
||||
/// </summary>
|
||||
public bool UseAdvancedScheduling { get; set; }
|
||||
|
||||
public List<CleanCategory> Categories { get; set; } = [];
|
||||
public List<SeedingRule> Categories { get; set; } = [];
|
||||
|
||||
public bool DeletePrivate { get; set; }
|
||||
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public required double MaxSeedTime { get; init; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to delete the source files when cleaning the download.
|
||||
/// </summary>
|
||||
public required bool DeleteSourceFiles { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Name.Trim()))
|
||||
@@ -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',
|
||||
|
||||
@@ -228,8 +228,8 @@
|
||||
|
||||
<div class="category-field">
|
||||
<label>
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('maxSeedTime')"
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('maxSeedTime')"
|
||||
title="Click for documentation"></i>
|
||||
Max Seed Time (hours)
|
||||
</label>
|
||||
@@ -243,6 +243,19 @@
|
||||
<small class="form-helper-text">Maximum time to seed before removing (<code>-1</code> means disabled)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-field">
|
||||
<label>
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('deleteSourceFiles')"
|
||||
title="Click for documentation"></i>
|
||||
Delete Source Files
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="deleteSourceFiles" [binary]="true" [inputId]="'deleteSourceFiles_' + i"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, the source files will be deleted when the download is removed</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Error for both maxRatio and maxSeedTime disabled -->
|
||||
<small *ngIf="hasCategoryGroupError(i, 'bothDisabled')" class="form-error-text">
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,15 @@ Maximum time in hours to seed before removing a download regardless of ratio. Se
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
title="Delete Source Files"
|
||||
icon="🗑️"
|
||||
>
|
||||
|
||||
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.
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
|
||||
Reference in New Issue
Block a user