diff --git a/code/Common/Configuration/DTOs/Arr/ArrInstanceDto.cs b/code/Common/Configuration/DTOs/Arr/ArrInstanceDto.cs
index 89657a84..8b482ac8 100644
--- a/code/Common/Configuration/DTOs/Arr/ArrInstanceDto.cs
+++ b/code/Common/Configuration/DTOs/Arr/ArrInstanceDto.cs
@@ -8,7 +8,7 @@ public class ArrInstanceDto
///
/// Unique identifier for this instance
///
- public Guid Id { get; set; } = Guid.NewGuid();
+ public Guid? Id { get; set; } = Guid.NewGuid();
///
/// Friendly name for this instance
diff --git a/code/Common/Configuration/DTOs/DownloadClient/DownloadClientConfigDto.cs b/code/Common/Configuration/DTOs/DownloadClient/DownloadClientConfigDto.cs
index 854c7967..55caca4a 100644
--- a/code/Common/Configuration/DTOs/DownloadClient/DownloadClientConfigDto.cs
+++ b/code/Common/Configuration/DTOs/DownloadClient/DownloadClientConfigDto.cs
@@ -5,7 +5,7 @@ namespace Common.Configuration.DTOs.DownloadClient;
///
/// DTO for retrieving DownloadClient configuration (excludes sensitive data)
///
-public class DownloadClientConfigDto
+public class DownloadClientConfigDto
{
///
/// Collection of download clients configured for the application
@@ -26,7 +26,7 @@ public class ClientConfigDto
///
/// Unique identifier for this client
///
- public Guid Id { get; set; } = Guid.NewGuid();
+ public Guid? Id { get; set; } = Guid.NewGuid();
///
/// Friendly name for this client
@@ -36,12 +36,12 @@ public class ClientConfigDto
///
/// Type of download client
///
- public DownloadClientType Type { get; set; } = DownloadClientType.None;
+ public required DownloadClientType Type { get; set; }
///
/// Host address for the download client
///
- public string Host { get; set; } = string.Empty;
+ public Uri? Host { get; set; }
///
/// Username for authentication (included without password)
diff --git a/code/Common/Configuration/DownloadClient/ClientConfig.cs b/code/Common/Configuration/DownloadClient/ClientConfig.cs
index 1c7de1c8..c2db665d 100644
--- a/code/Common/Configuration/DownloadClient/ClientConfig.cs
+++ b/code/Common/Configuration/DownloadClient/ClientConfig.cs
@@ -1,7 +1,6 @@
using Common.Attributes;
-using Common.Configuration;
using Common.Enums;
-using Microsoft.Extensions.Configuration;
+using Common.Exceptions;
using Newtonsoft.Json;
namespace Common.Configuration.DownloadClient;
@@ -29,12 +28,12 @@ public sealed record ClientConfig
///
/// Type of download client
///
- public DownloadClientType Type { get; init; } = DownloadClientType.None;
+ public required DownloadClientType Type { get; init; }
///
/// Host address for the download client
///
- public string Host { get; init; } = string.Empty;
+ public Uri? Host { get; init; }
///
/// Username for authentication
@@ -57,7 +56,7 @@ public sealed record ClientConfig
///
/// The computed full URL for the client
///
- public Uri Url => new($"{Host.TrimEnd('/')}/{UrlBase.TrimStart('/').TrimEnd('/')}");
+ public Uri Url => new($"{Host?.ToString().TrimEnd('/')}/{UrlBase.TrimStart('/').TrimEnd('/')}");
///
/// Validates the configuration
@@ -66,22 +65,17 @@ public sealed record ClientConfig
{
if (Id == Guid.Empty)
{
- throw new InvalidOperationException("Client ID cannot be empty");
+ throw new ValidationException("Client ID cannot be empty");
}
if (string.IsNullOrWhiteSpace(Name))
{
- throw new InvalidOperationException($"Client name cannot be empty for client ID: {Id}");
+ throw new ValidationException($"Client name cannot be empty for client ID: {Id}");
}
- if (string.IsNullOrWhiteSpace(Host))
+ if (Host is null && Type is not DownloadClientType.Usenet)
{
- throw new InvalidOperationException($"Host cannot be empty for client ID: {Id}");
- }
-
- if (Type == DownloadClientType.None)
- {
- throw new InvalidOperationException($"Client type must be specified for client ID: {Id}");
+ throw new ValidationException($"Host cannot be empty for client ID: {Id}");
}
}
}
diff --git a/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs b/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs
index 48e84145..cdcc898e 100644
--- a/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs
+++ b/code/Common/Configuration/DownloadClient/DownloadClientConfig.cs
@@ -1,4 +1,6 @@
-namespace Common.Configuration.DownloadClient;
+using Common.Exceptions;
+
+namespace Common.Configuration.DownloadClient;
public sealed record DownloadClientConfig : IConfig
{
@@ -32,34 +34,21 @@ public sealed record DownloadClientConfig : IConfig
public void Validate()
{
// Validate clients have unique IDs
- var duplicateIds = Clients
- .GroupBy(c => c.Id)
+ var duplicateNames = Clients
+ .GroupBy(c => c.Name)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
- if (duplicateIds.Any())
+ if (duplicateNames.Any())
{
- throw new InvalidOperationException($"Duplicate client IDs found: {string.Join(", ", duplicateIds)}");
+ throw new ValidationException($"Duplicate client names found: {string.Join(", ", duplicateNames)}");
}
// Validate each client configuration
foreach (var client in Clients)
{
- if (client.Id == Guid.Empty)
- {
- throw new InvalidOperationException("Client ID cannot be empty");
- }
-
- if (string.IsNullOrWhiteSpace(client.Name))
- {
- throw new InvalidOperationException($"Client name cannot be empty for client ID: {client.Id}");
- }
-
- if (string.IsNullOrWhiteSpace(client.Host))
- {
- throw new InvalidOperationException($"Host cannot be empty for client ID: {client.Id}");
- }
+ client.Validate();
}
}
}
\ No newline at end of file
diff --git a/code/Common/Enums/DownloadClientType.cs b/code/Common/Enums/DownloadClientType.cs
index 7911ff23..fccdc6bf 100644
--- a/code/Common/Enums/DownloadClientType.cs
+++ b/code/Common/Enums/DownloadClientType.cs
@@ -5,6 +5,5 @@ public enum DownloadClientType
QBittorrent,
Deluge,
Transmission,
- None,
- Disabled
+ Usenet,
}
\ No newline at end of file
diff --git a/code/Executable/Controllers/ConfigurationController.cs b/code/Executable/Controllers/ConfigurationController.cs
index 6d9248f4..94a6df41 100644
--- a/code/Executable/Controllers/ConfigurationController.cs
+++ b/code/Executable/Controllers/ConfigurationController.cs
@@ -107,23 +107,24 @@ public class ConfigurationController : ControllerBase
public async Task UpdateQueueCleanerConfig([FromBody] QueueCleanerConfig dto)
{
// Get existing config
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
- // Apply updates from DTO
- dto.Adapt(config);
+ // Apply updates from DTO, preserving sensitive data if not provided
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save QueueCleaner configuration");
}
// Update the scheduler based on configuration changes
- await UpdateJobSchedule(config, JobType.QueueCleaner);
+ await UpdateJobSchedule(oldConfig, JobType.QueueCleaner);
return Ok(new { Message = "QueueCleaner configuration updated successfully" });
}
@@ -166,16 +167,17 @@ public class ConfigurationController : ControllerBase
public async Task UpdateContentBlockerConfig([FromBody] ContentBlockerConfigUpdateDto dto)
{
// Get existing config
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
- // Apply updates from DTO
- dto.Adapt(config);
+ // Apply updates from DTO, preserving sensitive data if not provided
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save ContentBlocker configuration");
@@ -188,41 +190,43 @@ public class ConfigurationController : ControllerBase
public async Task UpdateDownloadCleanerConfig([FromBody] DownloadCleanerConfig dto)
{
// Get existing config
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
- // Apply updates from DTO
- dto.Adapt(config);
+ // Apply updates from DTO, preserving sensitive data if not provided
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save DownloadCleaner configuration");
}
// Update the scheduler based on configuration changes
- await UpdateJobSchedule(config, JobType.DownloadCleaner);
+ await UpdateJobSchedule(oldConfig, JobType.DownloadCleaner);
return Ok(new { Message = "DownloadCleaner configuration updated successfully" });
}
[HttpPut("download_client")]
- public async Task UpdateDownloadClientConfig([FromBody] DownloadClientConfigUpdateDto dto)
+ public async Task UpdateDownloadClientConfig(DownloadClientConfigUpdateDto dto)
{
// Get existing config to preserve sensitive data
- var config = await _configManager.GetConfigurationAsync();
-
+ var oldConfig = await _configManager.GetConfigurationAsync();
+
// Apply updates from DTO, preserving sensitive data if not provided
- dto.Adapt(config);
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save DownloadClient configuration");
@@ -235,24 +239,24 @@ public class ConfigurationController : ControllerBase
public async Task UpdateGeneralConfig([FromBody] GeneralConfig dto)
{
// Get existing config to preserve sensitive data
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
// Apply updates from DTO, preserving sensitive data if not provided
- dto.Adapt(config);
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
-
- _loggingConfigManager.SetLogLevel(config.LogLevel);
-
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save General configuration");
}
+ _loggingConfigManager.SetLogLevel(oldConfig.LogLevel);
+
return Ok(new { Message = "General configuration updated successfully" });
}
@@ -260,16 +264,17 @@ public class ConfigurationController : ControllerBase
public async Task UpdateSonarrConfig([FromBody] SonarrConfigUpdateDto dto)
{
// Get existing config to preserve sensitive data
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
// Apply updates from DTO, preserving sensitive data if not provided
- dto.Adapt(config);
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save Sonarr configuration");
@@ -282,16 +287,17 @@ public class ConfigurationController : ControllerBase
public async Task UpdateRadarrConfig([FromBody] RadarrConfigUpdateDto dto)
{
// Get existing config to preserve sensitive data
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
// Apply updates from DTO, preserving sensitive data if not provided
- dto.Adapt(config);
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save Radarr configuration");
@@ -304,16 +310,17 @@ public class ConfigurationController : ControllerBase
public async Task UpdateLidarrConfig([FromBody] LidarrConfigUpdateDto dto)
{
// Get existing config to preserve sensitive data
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
// Apply updates from DTO, preserving sensitive data if not provided
- dto.Adapt(config);
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
// Validate the configuration
- config.Validate();
+ newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save Lidarr configuration");
@@ -326,13 +333,17 @@ public class ConfigurationController : ControllerBase
public async Task UpdateNotificationsConfig([FromBody] NotificationsConfigUpdateDto dto)
{
// Get existing config to preserve sensitive data
- var config = await _configManager.GetConfigurationAsync();
+ var oldConfig = await _configManager.GetConfigurationAsync();
// Apply updates from DTO, preserving sensitive data if not provided
- dto.Adapt(config);
+ var newConfig = oldConfig.Adapt();
+ newConfig = dto.Adapt(newConfig);
+
+ // Validate the configuration
+ // newConfig.Validate();
// Persist the configuration
- var result = await _configManager.SaveConfigurationAsync(config);
+ var result = await _configManager.SaveConfigurationAsync(newConfig);
if (!result)
{
return StatusCode(500, "Failed to save Notifications configuration");
diff --git a/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs b/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs
index 99dc7037..43ffbc1f 100644
--- a/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs
+++ b/code/Infrastructure.Tests/Http/DynamicHttpClientProviderFixture.cs
@@ -38,7 +38,7 @@ public class DynamicHttpClientProviderFixture : IDisposable
Name = "QBit Test",
Type = DownloadClientType.QBittorrent,
Enabled = true,
- Host = "http://localhost:8080",
+ Host = new("http://localhost:8080"),
Username = "admin",
Password = "adminadmin"
};
@@ -52,7 +52,7 @@ public class DynamicHttpClientProviderFixture : IDisposable
Name = "Transmission Test",
Type = DownloadClientType.Transmission,
Enabled = true,
- Host = "http://localhost:9091",
+ Host = new("http://localhost:9091"),
Username = "admin",
Password = "adminadmin",
UrlBase = "transmission"
@@ -67,7 +67,7 @@ public class DynamicHttpClientProviderFixture : IDisposable
Name = "Deluge Test",
Type = DownloadClientType.Deluge,
Enabled = true,
- Host = "http://localhost:8112",
+ Host = new("http://localhost:8112"),
Username = "admin",
Password = "deluge"
};
diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs
index 9335ae90..2e9633e3 100644
--- a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs
@@ -71,9 +71,6 @@ public abstract class DownloadService : IDownloadService
_cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
- // Initialize with default empty configuration
- _clientConfig = new ClientConfig();
-
_queueCleanerConfig = _configManager.GetConfiguration();
_downloadCleanerConfig = _configManager.GetConfiguration();
}
diff --git a/code/UI/src/app/settings/download-client/download-client-settings.component.html b/code/UI/src/app/settings/download-client/download-client-settings.component.html
index d5327108..efa1ba96 100644
--- a/code/UI/src/app/settings/download-client/download-client-settings.component.html
+++ b/code/UI/src/app/settings/download-client/download-client-settings.component.html
@@ -69,35 +69,43 @@
-
-
-
-
- Host is required
- Host must be a valid URL
- Host must use http or https protocol
+
+
+
+
+
+
+ Host is required
+ Host must be a valid URL
+ Host must use http or https protocol
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ Usenet client type is for categorization only. No connection details needed.
diff --git a/code/UI/src/app/settings/download-client/download-client-settings.component.ts b/code/UI/src/app/settings/download-client/download-client-settings.component.ts
index 2069bcee..17081733 100644
--- a/code/UI/src/app/settings/download-client/download-client-settings.component.ts
+++ b/code/UI/src/app/settings/download-client/download-client-settings.component.ts
@@ -54,7 +54,8 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
clientTypeOptions = [
{ label: "QBittorrent", value: DownloadClientType.QBittorrent },
{ label: "Deluge", value: DownloadClientType.Deluge },
- { label: "Transmission", value: DownloadClientType.Transmission }
+ { label: "Transmission", value: DownloadClientType.Transmission },
+ { label: "Usenet", value: DownloadClientType.Usenet }
];
// Clean up subscriptions
@@ -255,20 +256,35 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
*/
addClient(client: ClientConfig | null = null): void {
const clientsArray = this.downloadClientForm.get('clients') as FormArray;
+ const clientType = client?.type ?? DownloadClientType.QBittorrent;
+ const isUsenet = clientType === DownloadClientType.Usenet;
- clientsArray.push(
- this.formBuilder.group({
- enabled: [client?.enabled ?? true],
- id: [client?.id ?? ''],
- name: [client?.name ?? '', Validators.required],
- type: [client?.type ?? DownloadClientType.QBittorrent, Validators.required],
- host: [client?.host ?? '', [Validators.required, this.uriValidator]],
- username: [client?.username ?? ''],
- password: [client?.password ?? ''],
- urlBase: [client?.urlBase ?? '']
- })
- );
+ // Create the client form group with conditional validators based on client type
+ const clientFormGroup = this.formBuilder.group({
+ enabled: [client?.enabled ?? true],
+ id: [client?.id ?? ''],
+ name: [client?.name ?? '', Validators.required],
+ type: [clientType, Validators.required],
+ host: [client?.host ?? '', isUsenet ? [] : [Validators.required, this.uriValidator]],
+ username: [client?.username ?? ''],
+ password: [client?.password ?? ''],
+ urlBase: [client?.urlBase ?? '']
+ });
+ // Set up subscription to type changes to update validators
+ const typeControl = clientFormGroup.get('type');
+ if (typeControl) {
+ typeControl.valueChanges.pipe(
+ takeUntil(this.destroy$)
+ ).subscribe(newType => {
+ // Only update validators if newType is not null
+ if (newType !== null) {
+ this.updateValidatorsForClientType(clientFormGroup, newType);
+ }
+ });
+ }
+
+ clientsArray.push(clientFormGroup);
this.downloadClientForm.markAsDirty();
// Recalculate if actual changes exist by comparing with original values
this.hasActualChanges = this.formValuesChanged();
@@ -361,4 +377,30 @@ export class DownloadClientSettingsComponent implements OnDestroy, CanComponentD
return { invalidUri: true }; // Invalid URI
}
}
+
+ /**
+ * Checks if a client type is Usenet
+ */
+ isUsenetClient(clientType: DownloadClientType | null | undefined): boolean {
+ return clientType === DownloadClientType.Usenet;
+ }
+
+ /**
+ * Update validators for a client form group based on the client type
+ */
+ private updateValidatorsForClientType(clientFormGroup: FormGroup, clientType: DownloadClientType): void {
+ const hostControl = clientFormGroup.get('host');
+ if (!hostControl) return;
+
+ if (clientType === DownloadClientType.Usenet) {
+ // For Usenet, remove all validators
+ hostControl.clearValidators();
+ } else {
+ // For other client types, add required and URI validators
+ hostControl.setValidators([Validators.required, this.uriValidator]);
+ }
+
+ // Update validation state
+ hostControl.updateValueAndValidity();
+ }
}
diff --git a/code/UI/src/app/shared/models/enums.ts b/code/UI/src/app/shared/models/enums.ts
index 79296e7b..c347ecbe 100644
--- a/code/UI/src/app/shared/models/enums.ts
+++ b/code/UI/src/app/shared/models/enums.ts
@@ -5,6 +5,5 @@ export enum DownloadClientType {
QBittorrent = 0,
Deluge = 1,
Transmission = 2,
- None = 3,
- Disabled = 4
+ Usenet = 3,
}