mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-24 06:28:55 -05:00
Compare commits
4 Commits
add_appris
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
375094862c | ||
|
|
58a72cef0f | ||
|
|
4ceff127a7 | ||
|
|
c07b811cf8 |
@@ -177,7 +177,7 @@ public class StatusController : ControllerBase
|
||||
try
|
||||
{
|
||||
var sonarrClient = _arrClientFactory.GetClient(InstanceType.Sonarr);
|
||||
await sonarrClient.TestConnectionAsync(instance);
|
||||
await sonarrClient.HealthCheckAsync(instance);
|
||||
|
||||
sonarrStatus.Add(new
|
||||
{
|
||||
@@ -209,7 +209,7 @@ public class StatusController : ControllerBase
|
||||
try
|
||||
{
|
||||
var radarrClient = _arrClientFactory.GetClient(InstanceType.Radarr);
|
||||
await radarrClient.TestConnectionAsync(instance);
|
||||
await radarrClient.HealthCheckAsync(instance);
|
||||
|
||||
radarrStatus.Add(new
|
||||
{
|
||||
@@ -241,7 +241,7 @@ public class StatusController : ControllerBase
|
||||
try
|
||||
{
|
||||
var lidarrClient = _arrClientFactory.GetClient(InstanceType.Lidarr);
|
||||
await lidarrClient.TestConnectionAsync(instance);
|
||||
await lidarrClient.HealthCheckAsync(instance);
|
||||
|
||||
lidarrStatus.Add(new
|
||||
{
|
||||
|
||||
@@ -44,8 +44,8 @@ public static class ServicesDI
|
||||
.AddScoped<IDownloadHunter, DownloadHunter>()
|
||||
.AddScoped<IFilenameEvaluator, FilenameEvaluator>()
|
||||
.AddScoped<IHardLinkFileService, HardLinkFileService>()
|
||||
.AddScoped<UnixHardLinkFileService>()
|
||||
.AddScoped<WindowsHardLinkFileService>()
|
||||
.AddScoped<IUnixHardLinkFileService, UnixHardLinkFileService>()
|
||||
.AddScoped<IWindowsHardLinkFileService, WindowsHardLinkFileService>()
|
||||
.AddScoped<IArrQueueIterator, ArrQueueIterator>()
|
||||
.AddScoped<IDownloadServiceFactory, DownloadServiceFactory>()
|
||||
.AddScoped<IStriker, Striker>()
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
|
||||
namespace Cleanuparr.Api.Features.Arr.Contracts.Requests;
|
||||
|
||||
public sealed record TestArrInstanceRequest
|
||||
{
|
||||
[Required]
|
||||
public required string Url { get; init; }
|
||||
|
||||
[Required]
|
||||
public required string ApiKey { get; init; }
|
||||
|
||||
public ArrInstance ToTestInstance() => new()
|
||||
{
|
||||
Enabled = true,
|
||||
Name = "Test Instance",
|
||||
Url = new Uri(Url),
|
||||
ApiKey = ApiKey,
|
||||
ArrConfigId = Guid.Empty,
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using Cleanuparr.Api.Features.Arr.Contracts.Requests;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Dtos;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Mapster;
|
||||
@@ -20,13 +21,16 @@ public sealed class ArrConfigController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<ArrConfigController> _logger;
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly IArrClientFactory _arrClientFactory;
|
||||
|
||||
public ArrConfigController(
|
||||
ILogger<ArrConfigController> logger,
|
||||
DataContext dataContext)
|
||||
DataContext dataContext,
|
||||
IArrClientFactory arrClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataContext = dataContext;
|
||||
_arrClientFactory = arrClientFactory;
|
||||
}
|
||||
|
||||
[HttpGet("sonarr")]
|
||||
@@ -124,6 +128,26 @@ public sealed class ArrConfigController : ControllerBase
|
||||
public Task<IActionResult> DeleteWhisparrInstance(Guid id)
|
||||
=> DeleteArrInstance(InstanceType.Whisparr, id);
|
||||
|
||||
[HttpPost("sonarr/instances/test")]
|
||||
public Task<IActionResult> TestSonarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Sonarr, request);
|
||||
|
||||
[HttpPost("radarr/instances/test")]
|
||||
public Task<IActionResult> TestRadarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Radarr, request);
|
||||
|
||||
[HttpPost("lidarr/instances/test")]
|
||||
public Task<IActionResult> TestLidarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Lidarr, request);
|
||||
|
||||
[HttpPost("readarr/instances/test")]
|
||||
public Task<IActionResult> TestReadarrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Readarr, request);
|
||||
|
||||
[HttpPost("whisparr/instances/test")]
|
||||
public Task<IActionResult> TestWhisparrInstance([FromBody] TestArrInstanceRequest request)
|
||||
=> TestArrInstance(InstanceType.Whisparr, request);
|
||||
|
||||
private async Task<IActionResult> GetArrConfig(InstanceType type)
|
||||
{
|
||||
await DataContext.Lock.WaitAsync();
|
||||
@@ -260,6 +284,23 @@ public sealed class ArrConfigController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IActionResult> TestArrInstance(InstanceType type, TestArrInstanceRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var testInstance = request.ToTestInstance();
|
||||
var client = _arrClientFactory.GetClient(type);
|
||||
await client.HealthCheckAsync(testInstance);
|
||||
|
||||
return Ok(new { Message = $"Connection to {type} instance successful" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test {Type} instance connection", type);
|
||||
return BadRequest(new { Message = $"Connection failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetConfigActionName(InstanceType type) => type switch
|
||||
{
|
||||
InstanceType.Sonarr => nameof(GetSonarrConfig),
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -26,30 +24,9 @@ public record UpdateDownloadCleanerConfigRequest
|
||||
|
||||
public bool UnlinkedUseTag { get; init; }
|
||||
|
||||
public string UnlinkedIgnoredRootDir { get; init; } = string.Empty;
|
||||
public List<string> UnlinkedIgnoredRootDirs { get; init; } = [];
|
||||
|
||||
public List<string> UnlinkedCategories { get; init; } = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -80,22 +80,23 @@ public sealed class DownloadCleanerConfigController : ControllerBase
|
||||
oldConfig.UnlinkedEnabled = newConfigDto.UnlinkedEnabled;
|
||||
oldConfig.UnlinkedTargetCategory = newConfigDto.UnlinkedTargetCategory;
|
||||
oldConfig.UnlinkedUseTag = newConfigDto.UnlinkedUseTag;
|
||||
oldConfig.UnlinkedIgnoredRootDir = newConfigDto.UnlinkedIgnoredRootDir;
|
||||
oldConfig.UnlinkedIgnoredRootDirs = newConfigDto.UnlinkedIgnoredRootDirs;
|
||||
oldConfig.UnlinkedCategories = newConfigDto.UnlinkedCategories;
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Domain.Exceptions;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
|
||||
namespace Cleanuparr.Api.Features.DownloadClient.Contracts.Requests;
|
||||
|
||||
public sealed record TestDownloadClientRequest
|
||||
{
|
||||
public DownloadClientTypeName TypeName { get; init; }
|
||||
|
||||
public DownloadClientType Type { get; init; }
|
||||
|
||||
public Uri? Host { get; init; }
|
||||
|
||||
public string? Username { get; init; }
|
||||
|
||||
public string? Password { get; init; }
|
||||
|
||||
public string? UrlBase { get; init; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (Host is null)
|
||||
{
|
||||
throw new ValidationException("Host cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadClientConfig ToTestConfig() => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Enabled = true,
|
||||
Name = "Test Client",
|
||||
TypeName = TypeName,
|
||||
Type = Type,
|
||||
Host = Host,
|
||||
Username = Username,
|
||||
Password = Password,
|
||||
UrlBase = UrlBase,
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Linq;
|
||||
|
||||
using Cleanuparr.Api.Features.DownloadClient.Contracts.Requests;
|
||||
using Cleanuparr.Infrastructure.Features.DownloadClient;
|
||||
using Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration;
|
||||
@@ -18,15 +19,18 @@ public sealed class DownloadClientController : ControllerBase
|
||||
private readonly ILogger<DownloadClientController> _logger;
|
||||
private readonly DataContext _dataContext;
|
||||
private readonly IDynamicHttpClientFactory _dynamicHttpClientFactory;
|
||||
private readonly IDownloadServiceFactory _downloadServiceFactory;
|
||||
|
||||
public DownloadClientController(
|
||||
ILogger<DownloadClientController> logger,
|
||||
DataContext dataContext,
|
||||
IDynamicHttpClientFactory dynamicHttpClientFactory)
|
||||
IDynamicHttpClientFactory dynamicHttpClientFactory,
|
||||
IDownloadServiceFactory downloadServiceFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataContext = dataContext;
|
||||
_dynamicHttpClientFactory = dynamicHttpClientFactory;
|
||||
_downloadServiceFactory = downloadServiceFactory;
|
||||
}
|
||||
|
||||
[HttpGet("download_client")]
|
||||
@@ -146,4 +150,33 @@ public sealed class DownloadClientController : ControllerBase
|
||||
DataContext.Lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("download_client/test")]
|
||||
public async Task<IActionResult> TestDownloadClient([FromBody] TestDownloadClientRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
request.Validate();
|
||||
|
||||
var testConfig = request.ToTestConfig();
|
||||
using var downloadService = _downloadServiceFactory.GetDownloadService(testConfig);
|
||||
var healthResult = await downloadService.HealthCheckAsync();
|
||||
|
||||
if (healthResult.IsHealthy)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
Message = $"Connection to {request.TypeName} successful",
|
||||
ResponseTime = healthResult.ResponseTime.TotalMilliseconds
|
||||
});
|
||||
}
|
||||
|
||||
return BadRequest(new { Message = healthResult.ErrorMessage ?? "Connection failed" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test {TypeName} client connection", request.TypeName);
|
||||
return BadRequest(new { Message = $"Connection failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,12 +586,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Notifiarr provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,12 +627,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Apprise provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,12 +673,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Ntfy provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -899,12 +899,12 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
};
|
||||
|
||||
await _notificationService.SendTestNotificationAsync(providerDto);
|
||||
return Ok(new { Message = "Test notification sent successfully", Success = true });
|
||||
return Ok(new { Message = "Test notification sent successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to test Pushover provider");
|
||||
throw;
|
||||
return BadRequest(new { Message = $"Test failed: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -696,7 +716,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UnlinkedTargetCategory = "unlinked",
|
||||
UnlinkedIgnoredRootDir = "/ignore"
|
||||
UnlinkedIgnoredRootDirs = ["/ignore"]
|
||||
};
|
||||
ContextProvider.Set(nameof(DownloadCleanerConfig), config);
|
||||
|
||||
@@ -724,7 +744,7 @@ public class DelugeServiceDCTests : IClassFixture<DelugeServiceFixture>
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.PopulateFileCounts("/ignore"),
|
||||
x => x.PopulateFileCounts(It.Is<IEnumerable<string>>(dirs => dirs.Contains("/ignore"))),
|
||||
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(
|
||||
@@ -900,7 +900,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
Id = Guid.NewGuid(),
|
||||
UnlinkedUseTag = false,
|
||||
UnlinkedTargetCategory = "unlinked",
|
||||
UnlinkedIgnoredRootDir = "/ignore"
|
||||
UnlinkedIgnoredRootDirs = ["/ignore"]
|
||||
};
|
||||
ContextProvider.Set(nameof(DownloadCleanerConfig), config);
|
||||
|
||||
@@ -925,7 +925,7 @@ public class QBitServiceDCTests : IClassFixture<QBitServiceFixture>
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.PopulateFileCounts("/ignore"),
|
||||
x => x.PopulateFileCounts(It.Is<IEnumerable<string>>(dirs => dirs.Contains("/ignore"))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -787,7 +787,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UnlinkedTargetCategory = "unlinked",
|
||||
UnlinkedIgnoredRootDir = "/ignore"
|
||||
UnlinkedIgnoredRootDirs = ["/ignore"]
|
||||
};
|
||||
ContextProvider.Set(nameof(DownloadCleanerConfig), config);
|
||||
|
||||
@@ -813,7 +813,7 @@ public class TransmissionServiceDCTests : IClassFixture<TransmissionServiceFixtu
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.PopulateFileCounts("/ignore"),
|
||||
x => x.PopulateFileCounts(It.Is<IEnumerable<string>>(dirs => dirs.Contains("/ignore"))),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -619,7 +639,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UnlinkedTargetCategory = "unlinked",
|
||||
UnlinkedIgnoredRootDir = "/ignore"
|
||||
UnlinkedIgnoredRootDirs = ["/ignore"]
|
||||
};
|
||||
ContextProvider.Set(nameof(DownloadCleanerConfig), config);
|
||||
|
||||
@@ -646,7 +666,7 @@ public class UTorrentServiceDCTests : IClassFixture<UTorrentServiceFixture>
|
||||
|
||||
// Assert
|
||||
_fixture.HardLinkFileService.Verify(
|
||||
x => x.PopulateFileCounts("/ignore"),
|
||||
x => x.PopulateFileCounts(It.Is<IEnumerable<string>>(dirs => dirs.Contains("/ignore"))),
|
||||
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;
|
||||
|
||||
@@ -168,16 +168,12 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the connection to an Arr instance
|
||||
/// </summary>
|
||||
/// <param name="arrInstance">The instance to test connection to</param>
|
||||
/// <returns>Task that completes when the connection test is done</returns>
|
||||
public virtual async Task TestConnectionAsync(ArrInstance arrInstance)
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HealthCheckAsync(ArrInstance arrInstance)
|
||||
{
|
||||
UriBuilder uriBuilder = new(arrInstance.Url);
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/v3/system/status";
|
||||
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}{GetSystemStatusUrlPath()}";
|
||||
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, uriBuilder.Uri);
|
||||
SetApiKey(request, arrInstance.ApiKey);
|
||||
@@ -188,6 +184,8 @@ public abstract class ArrClient : IArrClient
|
||||
|
||||
_logger.LogDebug("Connection test successful for {url}", arrInstance.Url);
|
||||
}
|
||||
|
||||
protected abstract string GetSystemStatusUrlPath();
|
||||
|
||||
protected abstract string GetQueueUrlPath();
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ public interface IArrClient
|
||||
Task SearchItemsAsync(ArrInstance arrInstance, HashSet<SearchItem>? items);
|
||||
|
||||
bool IsRecordValid(QueueRecord record);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tests the connection to an Arr instance
|
||||
/// </summary>
|
||||
/// <param name="arrInstance">The instance to test connection to</param>
|
||||
/// <returns>Task that completes when the connection test is done</returns>
|
||||
Task TestConnectionAsync(ArrInstance arrInstance);
|
||||
Task HealthCheckAsync(ArrInstance arrInstance);
|
||||
}
|
||||
@@ -22,6 +22,11 @@ public class LidarrClient : ArrClient, ILidarrClient
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v1/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v1/queue";
|
||||
|
||||
@@ -21,6 +21,11 @@ public class RadarrClient : ArrClient, IRadarrClient
|
||||
) : base(logger, httpClientFactory, striker, dryRunInterceptor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v3/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
|
||||
@@ -21,6 +21,11 @@ public class ReadarrClient : ArrClient, IReadarrClient
|
||||
) : base(logger, httpClientFactory, striker, dryRunInterceptor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v1/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
|
||||
@@ -25,6 +25,11 @@ public class SonarrClient : ArrClient, ISonarrClient
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v3/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v3/queue";
|
||||
|
||||
@@ -25,6 +25,11 @@ public class WhisparrClient : ArrClient, IWhisparrClient
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetSystemStatusUrlPath()
|
||||
{
|
||||
return "/api/v3/system/status";
|
||||
}
|
||||
|
||||
protected override string GetQueueUrlPath()
|
||||
{
|
||||
return "/api/v3/queue";
|
||||
|
||||
@@ -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)
|
||||
@@ -65,9 +65,9 @@ public partial class DelugeService
|
||||
|
||||
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
||||
|
||||
if (!string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir))
|
||||
if (downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0)
|
||||
{
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDir);
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDirs);
|
||||
}
|
||||
|
||||
foreach (DelugeItemWrapper torrent in downloads.Cast<DelugeItemWrapper>())
|
||||
@@ -105,7 +105,7 @@ public partial class DelugeService
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService
|
||||
.GetHardLinkCount(filePath, !string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir));
|
||||
.GetHardLinkCount(filePath, downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
@@ -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)
|
||||
@@ -89,9 +89,9 @@ public partial class QBitService
|
||||
|
||||
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
||||
|
||||
if (!string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir))
|
||||
if (downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0)
|
||||
{
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDir);
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDirs);
|
||||
}
|
||||
|
||||
foreach (QBitItemWrapper torrent in downloads.Cast<QBitItemWrapper>())
|
||||
@@ -131,7 +131,7 @@ public partial class QBitService
|
||||
continue;
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, !string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir));
|
||||
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
@@ -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)
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Cleanuparr.Infrastructure.Features.DownloadClient.Transmission;
|
||||
/// </summary>
|
||||
public interface ITransmissionClientWrapper
|
||||
{
|
||||
Task<SessionInfo> GetSessionInformationAsync();
|
||||
Task<SessionInfo?> GetSessionInformationAsync();
|
||||
Task<TransmissionTorrents?> TorrentGetAsync(string[] fields, string? hash = null);
|
||||
Task TorrentSetAsync(TorrentSettings settings);
|
||||
Task TorrentSetLocationAsync(long[] ids, string location, bool move);
|
||||
|
||||
@@ -16,11 +16,18 @@ public sealed class TransmissionClientWrapper : ITransmissionClientWrapper
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public Task<SessionInfo> GetSessionInformationAsync()
|
||||
public Task<SessionInfo?> GetSessionInformationAsync()
|
||||
=> _client.GetSessionInformationAsync();
|
||||
|
||||
public Task<TransmissionTorrents?> TorrentGetAsync(string[] fields, string? hash = null)
|
||||
=> _client.TorrentGetAsync(fields, hash);
|
||||
{
|
||||
if (hash is null)
|
||||
{
|
||||
return _client.TorrentGetAsync(fields);
|
||||
}
|
||||
|
||||
return _client.TorrentGetAsync(fields, hash);
|
||||
}
|
||||
|
||||
public Task TorrentSetAsync(TorrentSettings settings)
|
||||
=> _client.TorrentSetAsync(settings);
|
||||
|
||||
@@ -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)
|
||||
@@ -59,9 +59,9 @@ public partial class TransmissionService
|
||||
|
||||
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
||||
|
||||
if (!string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir))
|
||||
if (downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0)
|
||||
{
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDir);
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDirs);
|
||||
}
|
||||
|
||||
foreach (TransmissionItemWrapper torrent in downloads.Cast<TransmissionItemWrapper>())
|
||||
@@ -95,7 +95,7 @@ public partial class TransmissionService
|
||||
|
||||
string filePath = string.Join(Path.DirectorySeparatorChar, Path.Combine(torrent.Info.DownloadDir, file.Name).Split(['\\', '/']));
|
||||
|
||||
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, !string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir));
|
||||
long hardlinkCount = _hardLinkFileService.GetHardLinkCount(filePath, downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
@@ -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)
|
||||
@@ -55,9 +55,9 @@ public partial class UTorrentService
|
||||
|
||||
var downloadCleanerConfig = ContextProvider.Get<DownloadCleanerConfig>(nameof(DownloadCleanerConfig));
|
||||
|
||||
if (!string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir))
|
||||
if (downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0)
|
||||
{
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDir);
|
||||
_hardLinkFileService.PopulateFileCounts(downloadCleanerConfig.UnlinkedIgnoredRootDirs);
|
||||
}
|
||||
|
||||
foreach (UTorrentItemWrapper torrent in downloads.Cast<UTorrentItemWrapper>())
|
||||
@@ -86,7 +86,7 @@ public partial class UTorrentService
|
||||
}
|
||||
|
||||
long hardlinkCount = _hardLinkFileService
|
||||
.GetHardLinkCount(filePath, !string.IsNullOrEmpty(downloadCleanerConfig.UnlinkedIgnoredRootDir));
|
||||
.GetHardLinkCount(filePath, downloadCleanerConfig.UnlinkedIgnoredRootDirs.Count > 0);
|
||||
|
||||
if (hardlinkCount < 0)
|
||||
{
|
||||
@@ -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)
|
||||
|
||||
@@ -6,13 +6,13 @@ namespace Cleanuparr.Infrastructure.Features.Files;
|
||||
public class HardLinkFileService : IHardLinkFileService
|
||||
{
|
||||
private readonly ILogger<HardLinkFileService> _logger;
|
||||
private readonly UnixHardLinkFileService _unixHardLinkFileService;
|
||||
private readonly WindowsHardLinkFileService _windowsHardLinkFileService;
|
||||
private readonly IUnixHardLinkFileService _unixHardLinkFileService;
|
||||
private readonly IWindowsHardLinkFileService _windowsHardLinkFileService;
|
||||
|
||||
public HardLinkFileService(
|
||||
ILogger<HardLinkFileService> logger,
|
||||
UnixHardLinkFileService unixHardLinkFileService,
|
||||
WindowsHardLinkFileService windowsHardLinkFileService
|
||||
IUnixHardLinkFileService unixHardLinkFileService,
|
||||
IWindowsHardLinkFileService windowsHardLinkFileService
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -23,16 +23,24 @@ public class HardLinkFileService : IHardLinkFileService
|
||||
public void PopulateFileCounts(string directoryPath)
|
||||
{
|
||||
_logger.LogTrace("populating file counts from {dir}", directoryPath);
|
||||
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
_windowsHardLinkFileService.PopulateFileCounts(directoryPath);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_unixHardLinkFileService.PopulateFileCounts(directoryPath);
|
||||
}
|
||||
|
||||
public void PopulateFileCounts(IEnumerable<string> directoryPaths)
|
||||
{
|
||||
foreach (var directoryPath in directoryPaths.Where(d => !string.IsNullOrEmpty(d)))
|
||||
{
|
||||
PopulateFileCounts(directoryPath);
|
||||
}
|
||||
}
|
||||
|
||||
public long GetHardLinkCount(string filePath, bool ignoreRootDir)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
|
||||
@@ -8,6 +8,12 @@ public interface IHardLinkFileService
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">The root directory where to search for hardlinks.</param>
|
||||
void PopulateFileCounts(string directoryPath);
|
||||
|
||||
/// <summary>
|
||||
/// Populates the inode counts for Unix and the file index counts for Windows from multiple directories.
|
||||
/// </summary>
|
||||
/// <param name="directoryPaths">The root directories where to search for hardlinks.</param>
|
||||
void PopulateFileCounts(IEnumerable<string> directoryPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Get the hardlink count of a file.
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Files;
|
||||
|
||||
public interface ISpecificFileService
|
||||
{
|
||||
/// <summary>
|
||||
/// Populates the inode counts for Unix and the file index counts for Windows.
|
||||
/// Needs to be called before <see cref="GetHardLinkCount"/> to populate the inode counts.
|
||||
/// </summary>
|
||||
/// <param name="directoryPath">The root directory where to search for hardlinks.</param>
|
||||
void PopulateFileCounts(string directoryPath);
|
||||
|
||||
/// <summary>
|
||||
/// Get the hardlink count of a file.
|
||||
/// </summary>
|
||||
/// <param name="filePath">File path.</param>
|
||||
/// <param name="ignoreRootDir">Whether to ignore hardlinks found in the same root dir.</param>
|
||||
/// <returns>-1 on error, 0 if there are no hardlinks and 1 otherwise.</returns>
|
||||
long GetHardLinkCount(string filePath, bool ignoreRootDir);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Files;
|
||||
|
||||
public interface IUnixHardLinkFileService : ISpecificFileService
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Cleanuparr.Infrastructure.Features.Files;
|
||||
|
||||
public interface IWindowsHardLinkFileService : ISpecificFileService
|
||||
{
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mono.Unix.Native;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Files;
|
||||
|
||||
public class UnixHardLinkFileService : IHardLinkFileService, IDisposable
|
||||
public class UnixHardLinkFileService : IUnixHardLinkFileService, IDisposable
|
||||
{
|
||||
private readonly ILogger<UnixHardLinkFileService> _logger;
|
||||
private readonly ConcurrentDictionary<ulong, int> _inodeCounts = new();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Features.Files;
|
||||
|
||||
public class WindowsHardLinkFileService : IHardLinkFileService, IDisposable
|
||||
public class WindowsHardLinkFileService : IWindowsHardLinkFileService, IDisposable
|
||||
{
|
||||
private readonly ILogger<WindowsHardLinkFileService> _logger;
|
||||
private readonly ConcurrentDictionary<ulong, int> _fileIndexCounts = new();
|
||||
|
||||
@@ -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",
|
||||
@@ -224,7 +224,7 @@ public sealed class DownloadCleanerConfigTests
|
||||
UnlinkedEnabled = true,
|
||||
UnlinkedTargetCategory = "cleanuparr-unlinked",
|
||||
UnlinkedCategories = ["movies"],
|
||||
UnlinkedIgnoredRootDir = "/non/existent/directory"
|
||||
UnlinkedIgnoredRootDirs = ["/non/existent/directory"]
|
||||
};
|
||||
|
||||
var exception = Should.Throw<ValidationException>(() => config.Validate());
|
||||
@@ -241,7 +241,7 @@ public sealed class DownloadCleanerConfigTests
|
||||
UnlinkedEnabled = true,
|
||||
UnlinkedTargetCategory = "cleanuparr-unlinked",
|
||||
UnlinkedCategories = ["movies"],
|
||||
UnlinkedIgnoredRootDir = ""
|
||||
UnlinkedIgnoredRootDirs = []
|
||||
};
|
||||
|
||||
Should.NotThrow(() => config.Validate());
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ChangeUnlinkedIgnoredRootDirType : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "unlinked_ignored_root_dir",
|
||||
table: "download_cleaner_configs",
|
||||
newName: "unlinked_ignored_root_dirs");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE download_cleaner_configs
|
||||
SET unlinked_ignored_root_dirs = CASE
|
||||
WHEN unlinked_ignored_root_dirs IS NULL OR unlinked_ignored_root_dirs = '' THEN '[]'
|
||||
ELSE '["' || unlinked_ignored_root_dirs || '"]'
|
||||
END
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE download_cleaner_configs
|
||||
SET unlinked_ignored_root_dirs = CASE
|
||||
WHEN unlinked_ignored_root_dirs = '[]' OR unlinked_ignored_root_dirs IS NULL THEN ''
|
||||
ELSE SUBSTR(unlinked_ignored_root_dirs, 3, LENGTH(unlinked_ignored_root_dirs) - 4)
|
||||
END
|
||||
""");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "unlinked_ignored_root_dirs",
|
||||
table: "download_cleaner_configs",
|
||||
newName: "unlinked_ignored_root_dir");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -176,10 +139,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_enabled");
|
||||
|
||||
b.Property<string>("UnlinkedIgnoredRootDir")
|
||||
b.PrimitiveCollection<string>("UnlinkedIgnoredRootDirs")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_ignored_root_dir");
|
||||
.HasColumnName("unlinked_ignored_root_dirs");
|
||||
|
||||
b.Property<string>("UnlinkedTargetCategory")
|
||||
.IsRequired()
|
||||
@@ -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; }
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed record DownloadCleanerConfig : IJobConfig
|
||||
|
||||
public bool UnlinkedUseTag { get; set; }
|
||||
|
||||
public string UnlinkedIgnoredRootDir { get; set; } = string.Empty;
|
||||
public List<string> UnlinkedIgnoredRootDirs { get; set; } = [];
|
||||
|
||||
public List<string> UnlinkedCategories { get; set; } = [];
|
||||
|
||||
@@ -87,9 +87,12 @@ public sealed record DownloadCleanerConfig : IJobConfig
|
||||
throw new ValidationException("Empty unlinked category filter found");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(UnlinkedIgnoredRootDir) && !Directory.Exists(UnlinkedIgnoredRootDir))
|
||||
foreach (var dir in UnlinkedIgnoredRootDirs.Where(d => !string.IsNullOrEmpty(d)))
|
||||
{
|
||||
throw new ValidationException($"{UnlinkedIgnoredRootDir} root directory does not exist");
|
||||
if (!Directory.Exists(dir))
|
||||
{
|
||||
throw new ValidationException($"{dir} root directory does not exist");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
@@ -8,8 +8,8 @@ import { RadarrConfig } from "../../shared/models/radarr-config.model";
|
||||
import { LidarrConfig } from "../../shared/models/lidarr-config.model";
|
||||
import { ReadarrConfig } from "../../shared/models/readarr-config.model";
|
||||
import { WhisparrConfig } from "../../shared/models/whisparr-config.model";
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto } from "../../shared/models/download-client-config.model";
|
||||
import { ArrInstance, CreateArrInstanceDto } from "../../shared/models/arr-config.model";
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto, TestDownloadClientRequest, TestConnectionResult } from "../../shared/models/download-client-config.model";
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
import { GeneralConfig } from "../../shared/models/general-config.model";
|
||||
import {
|
||||
StallRule,
|
||||
@@ -765,4 +765,84 @@ export class ConfigurationService {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ===== CONNECTION TESTING =====
|
||||
|
||||
/**
|
||||
* Test a Sonarr instance connection
|
||||
*/
|
||||
testSonarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/sonarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Sonarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Radarr instance connection
|
||||
*/
|
||||
testRadarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/radarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Radarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Lidarr instance connection
|
||||
*/
|
||||
testLidarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/lidarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Lidarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Readarr instance connection
|
||||
*/
|
||||
testReadarrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/readarr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Readarr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a Whisparr instance connection
|
||||
*/
|
||||
testWhisparrInstance(request: TestArrInstanceRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/whisparr/instances/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing Whisparr instance:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test a download client connection
|
||||
*/
|
||||
testDownloadClient(request: TestDownloadClientRequest): Observable<TestConnectionResult> {
|
||||
return this.http.post<TestConnectionResult>(this.ApplicationPathService.buildApiUrl('/configuration/download_client/test'), request).pipe(
|
||||
catchError((error) => {
|
||||
console.error("Error testing download client:", error);
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
return throwError(() => new Error(errorMessage));
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
@@ -320,17 +333,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ignored Root Directory -->
|
||||
<!-- Ignored Root Directories -->
|
||||
<div class="field-row">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('unlinkedIgnoredRootDir')"
|
||||
<i class="pi pi-question-circle field-info-icon"
|
||||
(click)="openFieldDocs('unlinkedIgnoredRootDirs')"
|
||||
title="Click for documentation"></i>
|
||||
Ignored Root Directory
|
||||
Ignored Root Directories
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<input type="text" pInputText formControlName="unlinkedIgnoredRootDir" placeholder="/path/to/directory" />
|
||||
<small class="form-helper-text">Root directory to ignore when checking for unlinked downloads (used for cross-seed)</small>
|
||||
<app-mobile-autocomplete
|
||||
formControlName="unlinkedIgnoredRootDirs"
|
||||
placeholder="Add directory path"
|
||||
></app-mobile-autocomplete>
|
||||
<small class="form-helper-text">Root directories to ignore when checking for unlinked downloads (used for cross-seed)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
unlinkedEnabled: [{ value: false, disabled: true }],
|
||||
unlinkedTargetCategory: [{ value: 'cleanuparr-unlinked', disabled: true }, [Validators.required]],
|
||||
unlinkedUseTag: [{ value: false, disabled: true }],
|
||||
unlinkedIgnoredRootDir: [{ value: '', disabled: true }],
|
||||
unlinkedIgnoredRootDirs: [{ value: [], disabled: true }],
|
||||
unlinkedCategories: [{ value: [], disabled: true }]
|
||||
}, { validators: [this.validateUnlinkedCategories, this.validateAtLeastOneFeature] });
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -340,7 +341,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
unlinkedEnabled: config.unlinkedEnabled,
|
||||
unlinkedTargetCategory: config.unlinkedTargetCategory,
|
||||
unlinkedUseTag: config.unlinkedUseTag,
|
||||
unlinkedIgnoredRootDir: config.unlinkedIgnoredRootDir,
|
||||
unlinkedIgnoredRootDirs: config.unlinkedIgnoredRootDirs || [],
|
||||
unlinkedCategories: config.unlinkedCategories || []
|
||||
});
|
||||
|
||||
@@ -623,7 +624,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
unlinkedEnabled: formValues.unlinkedEnabled,
|
||||
unlinkedTargetCategory: formValues.unlinkedTargetCategory,
|
||||
unlinkedUseTag: formValues.unlinkedUseTag,
|
||||
unlinkedIgnoredRootDir: formValues.unlinkedIgnoredRootDir,
|
||||
unlinkedIgnoredRootDirs: formValues.unlinkedIgnoredRootDirs || [],
|
||||
unlinkedCategories: formValues.unlinkedCategories || []
|
||||
};
|
||||
|
||||
@@ -681,7 +682,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
unlinkedEnabled: false,
|
||||
unlinkedTargetCategory: 'cleanuparr-unlinked',
|
||||
unlinkedUseTag: false,
|
||||
unlinkedIgnoredRootDir: '',
|
||||
unlinkedIgnoredRootDirs: [],
|
||||
unlinkedCategories: []
|
||||
});
|
||||
|
||||
@@ -792,7 +793,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
private updateUnlinkedControlsState(enabled: boolean): void {
|
||||
const targetCategoryControl = this.downloadCleanerForm.get('unlinkedTargetCategory');
|
||||
const useTagControl = this.downloadCleanerForm.get('unlinkedUseTag');
|
||||
const ignoredRootDirControl = this.downloadCleanerForm.get('unlinkedIgnoredRootDir');
|
||||
const ignoredRootDirsControl = this.downloadCleanerForm.get('unlinkedIgnoredRootDirs');
|
||||
const categoriesControl = this.downloadCleanerForm.get('unlinkedCategories');
|
||||
|
||||
// Disable emitting events during bulk changes
|
||||
@@ -802,13 +803,13 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent
|
||||
// Enable all unlinked controls
|
||||
targetCategoryControl?.enable(options);
|
||||
useTagControl?.enable(options);
|
||||
ignoredRootDirControl?.enable(options);
|
||||
ignoredRootDirsControl?.enable(options);
|
||||
categoriesControl?.enable(options);
|
||||
} else {
|
||||
// Disable all unlinked controls
|
||||
targetCategoryControl?.disable(options);
|
||||
useTagControl?.disable(options);
|
||||
ignoredRootDirControl?.disable(options);
|
||||
ignoredRootDirsControl?.disable(options);
|
||||
categoriesControl?.disable(options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { patchState, signalStore, withHooks, withMethods, withState } from '@ngrx/signals';
|
||||
import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto } from '../../shared/models/download-client-config.model';
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto, TestDownloadClientRequest, TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface DownloadClientConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
pendingOperations: number;
|
||||
testing: boolean;
|
||||
testingClientId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: DownloadClientConfigState = {
|
||||
@@ -18,7 +22,11 @@ const initialState: DownloadClientConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
pendingOperations: 0
|
||||
pendingOperations: 0,
|
||||
testing: false,
|
||||
testingClientId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -91,7 +99,41 @@ export class DownloadClientConfigStore extends signalStore(
|
||||
resetError() {
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingClientId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a download client connection
|
||||
*/
|
||||
testClient: rxMethod<{ request: TestDownloadClientRequest; clientId?: string }>(
|
||||
(params$: Observable<{ request: TestDownloadClientRequest; clientId?: string }>) => params$.pipe(
|
||||
tap(({ clientId }) => patchState(store, { testing: true, testingClientId: clientId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testDownloadClient(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
/**
|
||||
* Batch create multiple clients
|
||||
*/
|
||||
|
||||
@@ -68,20 +68,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || downloadClientSaving()"
|
||||
[loading]="testingClientId() === client.id"
|
||||
(click)="testClientFromList(client)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit client"
|
||||
tooltipPosition="left"
|
||||
[disabled]="downloadClientSaving()"
|
||||
(click)="openEditClientModal(client)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete client"
|
||||
tooltipPosition="left"
|
||||
@@ -248,17 +259,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeClientModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="clientForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testClientFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="clientForm.invalid || downloadClientSaving()"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Component, EventEmitter, OnDestroy, Output, effect, inject } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from "@angular/forms";
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { DownloadClientConfigStore } from "./download-client-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { ClientConfig, DownloadClientConfig, CreateDownloadClientDto } from "../../shared/models/download-client-config.model";
|
||||
import { ClientConfig, CreateDownloadClientDto, TestDownloadClientRequest } from "../../shared/models/download-client-config.model";
|
||||
import { DownloadClientType, DownloadClientTypeName } from "../../shared/models/enums";
|
||||
import { DocumentationService } from "../../core/services/documentation.service";
|
||||
|
||||
@@ -76,6 +76,10 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
|
||||
downloadClientLoading = this.downloadClientStore.loading;
|
||||
downloadClientError = this.downloadClientStore.error;
|
||||
downloadClientSaving = this.downloadClientStore.saving;
|
||||
testing = this.downloadClientStore.testing;
|
||||
testingClientId = this.downloadClientStore.testingClientId;
|
||||
testError = this.downloadClientStore.testError;
|
||||
testResult = this.downloadClientStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -113,6 +117,22 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
|
||||
.subscribe(() => {
|
||||
this.onClientTypeChange();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.downloadClientStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.downloadClientStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,4 +392,43 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
|
||||
openFieldDocs(fieldName: string): void {
|
||||
this.documentationService.openFieldDocumentation('download-client', fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test client connection from the modal (new or editing)
|
||||
*/
|
||||
testClientFromModal(): void {
|
||||
if (this.clientForm.invalid) {
|
||||
this.markFormGroupTouched(this.clientForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const formValue = this.clientForm.value;
|
||||
const testRequest: TestDownloadClientRequest = {
|
||||
typeName: formValue.typeName,
|
||||
type: this.mapTypeNameToType(formValue.typeName),
|
||||
host: formValue.host,
|
||||
username: formValue.username,
|
||||
password: formValue.password,
|
||||
urlBase: formValue.urlBase,
|
||||
};
|
||||
|
||||
this.downloadClientStore.testClient({ request: testRequest, clientId: this.editingClient?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test client connection from the list view (existing client)
|
||||
*/
|
||||
testClientFromList(client: ClientConfig): void {
|
||||
const testRequest: TestDownloadClientRequest = {
|
||||
typeName: client.typeName,
|
||||
type: client.type,
|
||||
host: client.host,
|
||||
username: client.username,
|
||||
password: client.password,
|
||||
urlBase: client.urlBase,
|
||||
};
|
||||
|
||||
this.downloadClientStore.testClient({ request: testRequest, clientId: client.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { LidarrConfig } from '../../shared/models/lidarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface LidarrConfigState {
|
||||
config: LidarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface LidarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: LidarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: LidarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class LidarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Lidarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testLidarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || lidarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="lidarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,19 +227,29 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || lidarrSaving()"
|
||||
[loading]="lidarrSaving()"
|
||||
(click)="saveInstance()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { LidarrConfigStore } from "./lidarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { LidarrConfig } from "../../shared/models/lidarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
lidarrLoading = this.lidarrStore.loading;
|
||||
lidarrError = this.lidarrStore.error;
|
||||
lidarrSaving = this.lidarrStore.saving;
|
||||
testing = this.lidarrStore.testing;
|
||||
testingInstanceId = this.lidarrStore.testingInstanceId;
|
||||
testError = this.lidarrStore.testError;
|
||||
testResult = this.lidarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.lidarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.lidarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class LidarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Lidarr Instance' : 'Edit Lidarr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.lidarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.lidarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,25 +211,17 @@ export class NotificationProviderConfigStore extends signalStore(
|
||||
},
|
||||
error: (error) => {
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage, // Test errors should NOT trigger "Not connected" state
|
||||
testResult: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage // Test errors should NOT trigger "Not connected" state
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError((error) => {
|
||||
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage, // Test errors should NOT trigger "Not connected" state
|
||||
testResult: {
|
||||
success: false,
|
||||
message: errorMessage
|
||||
}
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: errorMessage // Test errors should NOT trigger "Not connected" state
|
||||
});
|
||||
return EMPTY;
|
||||
})
|
||||
|
||||
@@ -130,16 +130,11 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea
|
||||
}
|
||||
});
|
||||
|
||||
// Setup effect to react to test results
|
||||
// Setup effect to react to test results (HTTP 200 = success)
|
||||
effect(() => {
|
||||
const result = this.testResult();
|
||||
if (result) {
|
||||
if (result.success) {
|
||||
this.notificationService.showSuccess(result.message || "Test notification sent successfully");
|
||||
} else {
|
||||
// Error handling is already done in the test error effect above
|
||||
// This just handles the success case
|
||||
}
|
||||
this.notificationService.showSuccess(result.message || "Test notification sent successfully");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { RadarrConfig } from '../../shared/models/radarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface RadarrConfigState {
|
||||
config: RadarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface RadarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: RadarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: RadarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class RadarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Radarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testRadarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || radarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="radarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || radarrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { RadarrConfigStore } from "./radarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { RadarrConfig } from "../../shared/models/radarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
radarrLoading = this.radarrStore.loading;
|
||||
radarrError = this.radarrStore.error;
|
||||
radarrSaving = this.radarrStore.saving;
|
||||
testing = this.radarrStore.testing;
|
||||
testingInstanceId = this.radarrStore.testingInstanceId;
|
||||
testError = this.radarrStore.testError;
|
||||
testResult = this.radarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.radarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.radarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class RadarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Radarr Instance' : 'Edit Radarr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.radarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.radarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { ReadarrConfig } from '../../shared/models/readarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface ReadarrConfigState {
|
||||
config: ReadarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface ReadarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: ReadarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: ReadarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class ReadarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Readarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testReadarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || readarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="readarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || readarrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { ReadarrConfigStore } from "./readarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { ReadarrConfig } from "../../shared/models/readarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
readarrLoading = this.readarrStore.loading;
|
||||
readarrError = this.readarrStore.error;
|
||||
readarrSaving = this.readarrStore.saving;
|
||||
testing = this.readarrStore.testing;
|
||||
testingInstanceId = this.readarrStore.testingInstanceId;
|
||||
testError = this.readarrStore.testError;
|
||||
testResult = this.readarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.readarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.readarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class ReadarrSettingsComponent implements OnDestroy, CanComponentDeactiva
|
||||
const control = parentControl.get(controlName);
|
||||
return control ? control.dirty && control.hasError(errorName) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.readarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.readarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { SonarrConfig } from '../../shared/models/sonarr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface SonarrConfigState {
|
||||
config: SonarrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface SonarrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: SonarrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: SonarrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -97,6 +106,40 @@ export class SonarrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Sonarr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testSonarrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || sonarrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="sonarrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || sonarrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { SonarrConfigStore } from "./sonarr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { SonarrConfig } from "../../shared/models/sonarr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
sonarrLoading = this.sonarrStore.loading;
|
||||
sonarrError = this.sonarrStore.error;
|
||||
sonarrSaving = this.sonarrStore.saving;
|
||||
testing = this.sonarrStore.testing;
|
||||
testingInstanceId = this.sonarrStore.testingInstanceId;
|
||||
testError = this.sonarrStore.testError;
|
||||
testResult = this.sonarrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.sonarrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.sonarrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -418,4 +438,34 @@ export class SonarrSettingsComponent implements OnDestroy, CanComponentDeactivat
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Sonarr Instance' : 'Edit Sonarr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.sonarrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.sonarrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { rxMethod } from '@ngrx/signals/rxjs-interop';
|
||||
import { WhisparrConfig } from '../../shared/models/whisparr-config.model';
|
||||
import { ConfigurationService } from '../../core/services/configuration.service';
|
||||
import { EMPTY, Observable, catchError, switchMap, tap, forkJoin, of } from 'rxjs';
|
||||
import { ArrInstance, CreateArrInstanceDto } from '../../shared/models/arr-config.model';
|
||||
import { ArrInstance, CreateArrInstanceDto, TestArrInstanceRequest } from '../../shared/models/arr-config.model';
|
||||
import { TestConnectionResult } from '../../shared/models/download-client-config.model';
|
||||
|
||||
export interface WhisparrConfigState {
|
||||
config: WhisparrConfig | null;
|
||||
@@ -12,6 +13,10 @@ export interface WhisparrConfigState {
|
||||
saving: boolean;
|
||||
error: string | null;
|
||||
instanceOperations: number;
|
||||
testing: boolean;
|
||||
testingInstanceId: string | null;
|
||||
testError: string | null;
|
||||
testResult: TestConnectionResult | null;
|
||||
}
|
||||
|
||||
const initialState: WhisparrConfigState = {
|
||||
@@ -19,7 +24,11 @@ const initialState: WhisparrConfigState = {
|
||||
loading: false,
|
||||
saving: false,
|
||||
error: null,
|
||||
instanceOperations: 0
|
||||
instanceOperations: 0,
|
||||
testing: false,
|
||||
testingInstanceId: null,
|
||||
testError: null,
|
||||
testResult: null
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -123,6 +132,40 @@ export class WhisparrConfigStore extends signalStore(
|
||||
patchState(store, { error: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset test state
|
||||
*/
|
||||
resetTestState() {
|
||||
patchState(store, { testing: false, testingInstanceId: null, testError: null, testResult: null });
|
||||
},
|
||||
|
||||
/**
|
||||
* Test a Whisparr instance connection
|
||||
*/
|
||||
testInstance: rxMethod<{ request: TestArrInstanceRequest; instanceId?: string }>(
|
||||
(params$: Observable<{ request: TestArrInstanceRequest; instanceId?: string }>) => params$.pipe(
|
||||
tap(({ instanceId }) => patchState(store, { testing: true, testingInstanceId: instanceId || null, testError: null, testResult: null })),
|
||||
switchMap(({ request }) => configService.testWhisparrInstance(request).pipe(
|
||||
tap({
|
||||
next: (result) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testResult: result,
|
||||
testError: null
|
||||
});
|
||||
},
|
||||
error: (error) => {
|
||||
patchState(store, {
|
||||
testing: false,
|
||||
testError: error.message || 'Connection test failed'
|
||||
});
|
||||
}
|
||||
}),
|
||||
catchError(() => EMPTY)
|
||||
))
|
||||
)
|
||||
),
|
||||
|
||||
// ===== INSTANCE MANAGEMENT =====
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,20 +111,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-actions">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-play"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Test connection"
|
||||
tooltipPosition="left"
|
||||
[disabled]="testing() || whisparrSaving()"
|
||||
[loading]="testingInstanceId() === instance.id"
|
||||
(click)="testInstanceFromList(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-pencil"
|
||||
class="p-button-rounded p-button-text p-button-sm mr-1"
|
||||
pTooltip="Edit instance"
|
||||
tooltipPosition="left"
|
||||
[disabled]="whisparrSaving()"
|
||||
(click)="openEditInstanceModal(instance)"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-text p-button-danger p-button-sm"
|
||||
pTooltip="Delete instance"
|
||||
tooltipPosition="left"
|
||||
@@ -216,17 +227,27 @@
|
||||
|
||||
<ng-template pTemplate="footer">
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Cancel"
|
||||
class="p-button-text"
|
||||
(click)="closeInstanceModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Test"
|
||||
icon="pi pi-play"
|
||||
class="p-button-outlined ml-2"
|
||||
[disabled]="instanceForm.invalid || testing()"
|
||||
[loading]="testing()"
|
||||
(click)="testInstanceFromModal()"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
label="Save"
|
||||
icon="pi pi-save"
|
||||
class="p-button-primary ml-2"
|
||||
[disabled]="instanceForm.invalid || whisparrSaving()"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||
import { WhisparrConfigStore } from "./whisparr-config.store";
|
||||
import { CanComponentDeactivate } from "../../core/guards";
|
||||
import { WhisparrConfig } from "../../shared/models/whisparr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance } from "../../shared/models/arr-config.model";
|
||||
import { CreateArrInstanceDto, ArrInstance, TestArrInstanceRequest } from "../../shared/models/arr-config.model";
|
||||
|
||||
// PrimeNG Components
|
||||
import { CardModule } from "primeng/card";
|
||||
@@ -74,6 +74,10 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
whisparrLoading = this.whisparrStore.loading;
|
||||
whisparrError = this.whisparrStore.error;
|
||||
whisparrSaving = this.whisparrStore.saving;
|
||||
testing = this.whisparrStore.testing;
|
||||
testingInstanceId = this.whisparrStore.testingInstanceId;
|
||||
testError = this.whisparrStore.testError;
|
||||
testResult = this.whisparrStore.testResult;
|
||||
|
||||
/**
|
||||
* Check if component can be deactivated (navigation guard)
|
||||
@@ -112,6 +116,22 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
.subscribe(() => {
|
||||
this.hasGlobalChanges = this.globalFormValuesChanged();
|
||||
});
|
||||
|
||||
// Setup effect to handle test results
|
||||
effect(() => {
|
||||
const testResult = this.testResult();
|
||||
const testError = this.testError();
|
||||
|
||||
if (testResult) {
|
||||
this.notificationService.showSuccess(testResult.message || 'Connection test successful');
|
||||
this.whisparrStore.resetTestState();
|
||||
}
|
||||
|
||||
if (testError) {
|
||||
this.notificationService.showError(testError);
|
||||
this.whisparrStore.resetTestState();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,4 +433,34 @@ export class WhisparrSettingsComponent implements OnDestroy, CanComponentDeactiv
|
||||
get modalTitle(): string {
|
||||
return this.modalMode === 'add' ? 'Add Whisparr Instance' : 'Edit Whisparr Instance';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the modal (new or editing)
|
||||
*/
|
||||
testInstanceFromModal(): void {
|
||||
if (this.instanceForm.invalid) {
|
||||
this.markFormGroupTouched(this.instanceForm);
|
||||
this.notificationService.showError('Please fix the validation errors before testing');
|
||||
return;
|
||||
}
|
||||
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: this.instanceForm.get('url')?.value,
|
||||
apiKey: this.instanceForm.get('apiKey')?.value,
|
||||
};
|
||||
|
||||
this.whisparrStore.testInstance({ request: testRequest, instanceId: this.editingInstance?.id });
|
||||
}
|
||||
|
||||
/**
|
||||
* Test instance connection from the list view (existing instance)
|
||||
*/
|
||||
testInstanceFromList(instance: ArrInstance): void {
|
||||
const testRequest: TestArrInstanceRequest = {
|
||||
url: instance.url,
|
||||
apiKey: instance.apiKey,
|
||||
};
|
||||
|
||||
this.whisparrStore.testInstance({ request: testRequest, instanceId: instance.id });
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,11 @@ export interface CreateArrInstanceDto {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for testing an Arr instance connection
|
||||
*/
|
||||
export interface TestArrInstanceRequest {
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface DownloadCleanerConfig {
|
||||
unlinkedEnabled: boolean;
|
||||
unlinkedTargetCategory: string;
|
||||
unlinkedUseTag: boolean;
|
||||
unlinkedIgnoredRootDir: string;
|
||||
unlinkedIgnoredRootDirs: string[];
|
||||
unlinkedCategories: string[];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,6 +56,6 @@ export const defaultDownloadCleanerConfig: DownloadCleanerConfig = {
|
||||
unlinkedEnabled: false,
|
||||
unlinkedTargetCategory: 'cleanuparr-unlinked',
|
||||
unlinkedUseTag: false,
|
||||
unlinkedIgnoredRootDir: '',
|
||||
unlinkedIgnoredRootDirs: [],
|
||||
unlinkedCategories: []
|
||||
};
|
||||
|
||||
@@ -124,3 +124,23 @@ export interface ClientConfigUpdateDto extends ClientConfig {
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request for testing a download client connection
|
||||
*/
|
||||
export interface TestDownloadClientRequest {
|
||||
typeName: DownloadClientTypeName;
|
||||
type: DownloadClientType;
|
||||
host?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
urlBase?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of testing a connection (HTTP 200 = success)
|
||||
*/
|
||||
export interface TestConnectionResult {
|
||||
message: string;
|
||||
responseTime?: number;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,5 @@ export interface AppriseConfiguration {
|
||||
}
|
||||
|
||||
export interface TestNotificationResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -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