diff --git a/code/Infrastructure/Health/HealthCheckService.cs b/code/Infrastructure/Health/HealthCheckService.cs index 463f274e..4706d2db 100644 --- a/code/Infrastructure/Health/HealthCheckService.cs +++ b/code/Infrastructure/Health/HealthCheckService.cs @@ -1,3 +1,4 @@ +using Common.Enums; using Data; using Infrastructure.Verticals.DownloadClient; using Microsoft.EntityFrameworkCore; @@ -61,50 +62,23 @@ public class HealthCheckService : IHealthCheckService // Get the client instance var client = _downloadServiceFactory.GetDownloadService(downloadClientConfig); - // Measure response time - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + // Execute the health check + var healthResult = await client.HealthCheckAsync(); - try + // Create health status object + var status = new HealthStatus { - // Execute the login to check connectivity - await client.LoginAsync(); - - stopwatch.Stop(); - - // Create health status object - var status = new HealthStatus - { - ClientId = clientId, - ClientName = downloadClientConfig.Name, - ClientTypeName = downloadClientConfig.TypeName, - IsHealthy = true, - LastChecked = DateTime.UtcNow, - ResponseTime = stopwatch.Elapsed - }; - - UpdateHealthStatus(status); - return status; - } - catch (Exception ex) - { - stopwatch.Stop(); - - _logger.LogWarning(ex, "Health check failed for client {clientId}", clientId); - - var status = new HealthStatus - { - ClientId = clientId, - ClientName = downloadClientConfig.Name, - ClientTypeName = downloadClientConfig.TypeName, - IsHealthy = false, - LastChecked = DateTime.UtcNow, - ErrorMessage = $"Connection failed: {ex.Message}", - ResponseTime = stopwatch.Elapsed - }; - - UpdateHealthStatus(status); - return status; - } + ClientId = clientId, + ClientName = downloadClientConfig.Name, + ClientTypeName = downloadClientConfig.TypeName, + IsHealthy = healthResult.IsHealthy, + LastChecked = DateTime.UtcNow, + ErrorMessage = healthResult.ErrorMessage, + ResponseTime = healthResult.ResponseTime + }; + + UpdateHealthStatus(status); + return status; } catch (Exception ex) { @@ -133,6 +107,7 @@ public class HealthCheckService : IHealthCheckService // Get all enabled client configurations var enabledClients = await _dataContext.DownloadClients .Where(x => x.Enabled) + .Where(x => x.TypeName != DownloadClientTypeName.Usenet) .ToListAsync(); var results = new Dictionary(); diff --git a/code/Infrastructure/Logging/LoggingInitializer.cs b/code/Infrastructure/Logging/LoggingInitializer.cs index f9b8f110..effd1b3b 100644 --- a/code/Infrastructure/Logging/LoggingInitializer.cs +++ b/code/Infrastructure/Logging/LoggingInitializer.cs @@ -31,7 +31,7 @@ public class LoggingInitializer : BackgroundService await _eventPublisher.PublishAsync( random.Next(0, 100) > 50 ? EventType.DownloadCleaned : EventType.StalledStrike, - "test", + "This is a very long message to test how it all looks in the frontend. This is just gibberish, but helps us figure out how the layout should be to display messages properly.", EventSeverity.Important, data: new { Hash = "hash", Name = "name", StrikeCount = "1", Type = "stalled" }); throw new Exception("test exception"); @@ -42,7 +42,7 @@ public class LoggingInitializer : BackgroundService _logger.LogTrace("test trace"); _logger.LogDebug("test debug"); _logger.LogWarning("test warn"); - _logger.LogError(exception, "test"); + _logger.LogError(exception, "This is a very long message to test how it all looks in the frontend. This is just gibberish, but helps us figure out how the layout should be to display messages properly."); } await Task.Delay(10000, stoppingToken); diff --git a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs index 9a40a360..289e4746 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs @@ -1,6 +1,5 @@ using Common.Configuration; using Common.Exceptions; -using Data; using Data.Models.Deluge.Response; using Infrastructure.Events; using Infrastructure.Interceptors; @@ -15,7 +14,7 @@ namespace Infrastructure.Verticals.DownloadClient.Deluge; public partial class DelugeService : DownloadService, IDelugeService { - private DelugeClient? _client; + private readonly DelugeClient _client; public DelugeService( ILogger logger, @@ -26,48 +25,19 @@ public partial class DelugeService : DownloadService, IDelugeService IHardLinkFileService hardLinkFileService, IDynamicHttpClientProvider httpClientProvider, EventPublisher eventPublisher, - BlocklistProvider blocklistProvider + BlocklistProvider blocklistProvider, + DownloadClientConfig downloadClientConfig ) : base( logger, cache, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService, - httpClientProvider, eventPublisher, blocklistProvider + httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig ) { - // Client will be initialized when Initialize() is called with a specific client configuration - // TODO initialize client & httpclient here - } - - /// - public override void Initialize(DownloadClientConfig downloadClientConfig) - { - // Initialize base service first - base.Initialize(downloadClientConfig); - - // Ensure client type is correct - if (downloadClientConfig.TypeName != Common.Enums.DownloadClientTypeName.Deluge) - { - throw new InvalidOperationException($"Cannot initialize DelugeService with client type {downloadClientConfig.TypeName}"); - } - - if (_httpClient == null) - { - throw new InvalidOperationException("HTTP client is not initialized"); - } - - // Create Deluge client _client = new DelugeClient(downloadClientConfig, _httpClient); - - _logger.LogInformation("Initialized Deluge service for client {clientName} ({clientId})", - downloadClientConfig.Name, downloadClientConfig.Id); } public override async Task LoginAsync() { - if (_client == null) - { - throw new InvalidOperationException("Deluge client is not initialized"); - } - try { await _client.LoginAsync(); @@ -86,6 +56,68 @@ public partial class DelugeService : DownloadService, IDelugeService } } + public override async Task HealthCheckAsync() + { + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + try + { + bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) || + !string.IsNullOrEmpty(_downloadClientConfig.Password); + + if (hasCredentials) + { + // If credentials are provided, we must be able to login and connect for the service to be healthy + await _client.LoginAsync(); + + if (!await _client.IsConnected() && !await _client.Connect()) + { + throw new Exception("Deluge WebUI is not connected to the daemon"); + } + + _logger.LogDebug("Health check: Successfully logged in to Deluge client {clientId}", _downloadClientConfig.Id); + } + else + { + // If no credentials, test basic connectivity to the web UI + // We'll try a simple HTTP request to verify the service is running + if (_httpClient == null) + { + throw new InvalidOperationException("HTTP client is not initialized"); + } + + var response = await _httpClient.GetAsync("/"); + if (!response.IsSuccessStatusCode && response.StatusCode != System.Net.HttpStatusCode.Unauthorized) + { + throw new Exception($"Service returned status code: {response.StatusCode}"); + } + + _logger.LogDebug("Health check: Successfully connected to Deluge client {clientId}", _downloadClientConfig.Id); + } + + stopwatch.Stop(); + + return new HealthCheckResult + { + IsHealthy = true, + ResponseTime = stopwatch.Elapsed + }; + } + catch (Exception ex) + { + stopwatch.Stop(); + + _logger.LogWarning(ex, "Health check failed for Deluge client {clientId}", _downloadClientConfig.Id); + + return new HealthCheckResult + { + IsHealthy = false, + ErrorMessage = $"Connection failed: {ex.Message}", + ResponseTime = stopwatch.Elapsed + }; + } + } + private static void ProcessFiles(Dictionary? contents, Action processFile) { if (contents is null) @@ -110,7 +142,5 @@ public partial class DelugeService : DownloadService, IDelugeService public override void Dispose() { - _client = null; - _httpClient?.Dispose(); } } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs index b19deaf7..62d71a15 100644 --- a/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/DownloadService.cs @@ -19,6 +19,13 @@ using Microsoft.Extensions.Logging; namespace Infrastructure.Verticals.DownloadClient; +public class HealthCheckResult +{ + public bool IsHealthy { get; set; } + public string? ErrorMessage { get; set; } + public TimeSpan ResponseTime { get; set; } +} + public abstract class DownloadService : IDownloadService { protected readonly ILogger _logger; @@ -28,17 +35,11 @@ public abstract class DownloadService : IDownloadService protected readonly MemoryCacheEntryOptions _cacheOptions; protected readonly IDryRunInterceptor _dryRunInterceptor; protected readonly IHardLinkFileService _hardLinkFileService; - protected readonly IDynamicHttpClientProvider _httpClientProvider; protected readonly EventPublisher _eventPublisher; protected readonly BlocklistProvider _blocklistProvider; - protected HttpClient? _httpClient; - + protected readonly HttpClient _httpClient; + protected readonly DownloadClientConfig _downloadClientConfig; - // Client-specific configuration - protected DownloadClientConfig _downloadClientConfig; - - // HTTP client for this service - protected DownloadService( ILogger logger, IMemoryCache cache, @@ -48,7 +49,8 @@ public abstract class DownloadService : IDownloadService IHardLinkFileService hardLinkFileService, IDynamicHttpClientProvider httpClientProvider, EventPublisher eventPublisher, - BlocklistProvider blocklistProvider + BlocklistProvider blocklistProvider, + DownloadClientConfig downloadClientConfig ) { _logger = logger; @@ -57,11 +59,12 @@ public abstract class DownloadService : IDownloadService _striker = striker; _dryRunInterceptor = dryRunInterceptor; _hardLinkFileService = hardLinkFileService; - _httpClientProvider = httpClientProvider; _eventPublisher = eventPublisher; _blocklistProvider = blocklistProvider; _cacheOptions = new MemoryCacheEntryOptions() .SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer); + _downloadClientConfig = downloadClientConfig; + _httpClient = httpClientProvider.CreateClient(downloadClientConfig); } /// @@ -70,22 +73,24 @@ public abstract class DownloadService : IDownloadService return _downloadClientConfig.Id; } - /// - public virtual void Initialize(DownloadClientConfig downloadClientConfig) - { - _downloadClientConfig = downloadClientConfig; - - // Create HTTP client for this service - _httpClient = _httpClientProvider.CreateClient(downloadClientConfig); - - _logger.LogDebug("Initialized download service for client {clientId} ({type})", - downloadClientConfig.Id, downloadClientConfig.TypeName); - } + // /// + // public virtual void Initialize(DownloadClientConfig downloadClientConfig) + // { + // _downloadClientConfig = downloadClientConfig; + // + // // Create HTTP client for this service + // _httpClient = _httpClientProvider.CreateClient(downloadClientConfig); + // + // _logger.LogDebug("Initialized download service for client {clientId} ({type})", + // downloadClientConfig.Id, downloadClientConfig.TypeName); + // } public abstract void Dispose(); public abstract Task LoginAsync(); + public abstract Task HealthCheckAsync(); + public abstract Task ShouldRemoveFromArrQueueAsync(string hash, IReadOnlyList ignoredDownloads); diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs index dd522649..8db087ac 100644 --- a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs +++ b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceFactory.cs @@ -1,8 +1,15 @@ using Common.Configuration; using Common.Enums; +using Infrastructure.Events; +using Infrastructure.Http; +using Infrastructure.Interceptors; +using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.DownloadClient.Deluge; using Infrastructure.Verticals.DownloadClient.QBittorrent; using Infrastructure.Verticals.DownloadClient.Transmission; +using Infrastructure.Verticals.Files; +using Infrastructure.Verticals.ItemStriker; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -63,23 +70,73 @@ public sealed class DownloadServiceFactory return downloadClientConfig.TypeName switch { - DownloadClientTypeName.QBittorrent => CreateClientService(downloadClientConfig), - DownloadClientTypeName.Deluge => CreateClientService(downloadClientConfig), - DownloadClientTypeName.Transmission => CreateClientService(downloadClientConfig), + DownloadClientTypeName.QBittorrent => CreateQBitService(downloadClientConfig), + DownloadClientTypeName.Deluge => CreateDelugeService(downloadClientConfig), + DownloadClientTypeName.Transmission => CreateTransmissionService(downloadClientConfig), _ => throw new NotSupportedException($"Download client type {downloadClientConfig.TypeName} is not supported") }; } - /// - /// Creates a download client service for a specific client type - /// - /// The type of download service to create - /// The client configuration - /// An implementation of IDownloadService - private T CreateClientService(DownloadClientConfig downloadClientConfig) where T : IDownloadService + private QBitService CreateQBitService(DownloadClientConfig downloadClientConfig) { - var service = _serviceProvider.GetRequiredService(); - service.Initialize(downloadClientConfig); + var logger = _serviceProvider.GetRequiredService>(); + var cache = _serviceProvider.GetRequiredService(); + var filenameEvaluator = _serviceProvider.GetRequiredService(); + var striker = _serviceProvider.GetRequiredService(); + var dryRunInterceptor = _serviceProvider.GetRequiredService(); + var hardLinkFileService = _serviceProvider.GetRequiredService(); + var httpClientProvider = _serviceProvider.GetRequiredService(); + var eventPublisher = _serviceProvider.GetRequiredService(); + var blocklistProvider = _serviceProvider.GetRequiredService(); + + // Create the QBitService instance + QBitService service = new( + logger, cache, filenameEvaluator, striker, dryRunInterceptor, + hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig + ); + + return service; + } + + private DelugeService CreateDelugeService(DownloadClientConfig downloadClientConfig) + { + var logger = _serviceProvider.GetRequiredService>(); + var filenameEvaluator = _serviceProvider.GetRequiredService(); + var cache = _serviceProvider.GetRequiredService(); + var striker = _serviceProvider.GetRequiredService(); + var dryRunInterceptor = _serviceProvider.GetRequiredService(); + var hardLinkFileService = _serviceProvider.GetRequiredService(); + var httpClientProvider = _serviceProvider.GetRequiredService(); + var eventPublisher = _serviceProvider.GetRequiredService(); + var blocklistProvider = _serviceProvider.GetRequiredService(); + + // Create the DelugeService instance + DelugeService service = new( + logger, cache, filenameEvaluator, striker, dryRunInterceptor, + hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig + ); + + return service; + } + + private TransmissionService CreateTransmissionService(DownloadClientConfig downloadClientConfig) + { + var logger = _serviceProvider.GetRequiredService>(); + var cache = _serviceProvider.GetRequiredService(); + var filenameEvaluator = _serviceProvider.GetRequiredService(); + var striker = _serviceProvider.GetRequiredService(); + var dryRunInterceptor = _serviceProvider.GetRequiredService(); + var hardLinkFileService = _serviceProvider.GetRequiredService(); + var httpClientProvider = _serviceProvider.GetRequiredService(); + var eventPublisher = _serviceProvider.GetRequiredService(); + var blocklistProvider = _serviceProvider.GetRequiredService(); + + // Create the TransmissionService instance + TransmissionService service = new( + logger, cache, filenameEvaluator, striker, dryRunInterceptor, + hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig + ); + return service; } } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs index 961fd7e6..e1302b17 100644 --- a/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/IDownloadService.cs @@ -1,11 +1,4 @@ -using System.Collections.Concurrent; -using System.Text.RegularExpressions; -using Common.Configuration; using Data.Models.Configuration.DownloadCleaner; -using Data.Models.Configuration.QueueCleaner; -using Data.Enums; -using Infrastructure.Interceptors; -using QBittorrent.Client; namespace Infrastructure.Verticals.DownloadClient; @@ -17,14 +10,14 @@ public interface IDownloadService : IDisposable /// The client ID Guid GetClientId(); - /// - /// Initializes the download service with client-specific configuration - /// - /// The client configuration - public void Initialize(DownloadClientConfig downloadClientConfig); - public Task LoginAsync(); + /// + /// Performs a health check on the download client + /// + /// The health check result + public Task HealthCheckAsync(); + /// /// Checks whether the download should be removed from the *arr queue. /// diff --git a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs index 560177ad..9385d741 100644 --- a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs @@ -14,11 +14,10 @@ namespace Infrastructure.Verticals.DownloadClient.QBittorrent; public partial class QBitService : DownloadService, IQBitService { - protected QBittorrentClient? _client; + protected readonly QBittorrentClient _client; public QBitService( ILogger logger, - IHttpClientFactory httpClientFactory, IMemoryCache cache, IFilenameEvaluator filenameEvaluator, IStriker striker, @@ -26,35 +25,18 @@ public partial class QBitService : DownloadService, IQBitService IHardLinkFileService hardLinkFileService, IDynamicHttpClientProvider httpClientProvider, EventPublisher eventPublisher, - BlocklistProvider blocklistProvider + BlocklistProvider blocklistProvider, + DownloadClientConfig downloadClientConfig ) : base( logger, cache, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService, - httpClientProvider, eventPublisher, blocklistProvider + httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig ) { - // Client will be initialized when Initialize() is called with a specific client configuration + _client = new QBittorrentClient(_httpClient, downloadClientConfig.Url); } - /// - public override void Initialize(DownloadClientConfig downloadClientConfig) - { - // Initialize base service first - base.Initialize(downloadClientConfig); - - // Create QBittorrent client - _client = new QBittorrentClient(_httpClient, downloadClientConfig.Url); - - _logger.LogInformation("Initialized QBittorrent service for client {clientName} ({clientId})", - downloadClientConfig.Name, downloadClientConfig.Id); - } - public override async Task LoginAsync() { - if (_client == null) - { - throw new InvalidOperationException("QBittorrent client is not initialized"); - } - if (string.IsNullOrEmpty(_downloadClientConfig.Username) && string.IsNullOrEmpty(_downloadClientConfig.Password)) { _logger.LogDebug("No credentials configured for client {clientId}, skipping login", _downloadClientConfig.Id); @@ -72,14 +54,54 @@ public partial class QBitService : DownloadService, IQBitService throw; } } + + public override async Task HealthCheckAsync() + { + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + try + { + bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) || + !string.IsNullOrEmpty(_downloadClientConfig.Password); + + if (hasCredentials) + { + // If credentials are provided, we must be able to login for the service to be healthy + await _client.LoginAsync(_downloadClientConfig.Username, _downloadClientConfig.Password); + _logger.LogDebug("Health check: Successfully logged in to QBittorrent client {clientId}", _downloadClientConfig.Id); + } + else + { + // If no credentials, test connectivity using version endpoint + await _client.GetApiVersionAsync(); + _logger.LogDebug("Health check: Successfully connected to QBittorrent client {clientId}", _downloadClientConfig.Id); + } + + stopwatch.Stop(); + + return new HealthCheckResult + { + IsHealthy = true, + ResponseTime = stopwatch.Elapsed + }; + } + catch (Exception ex) + { + stopwatch.Stop(); + + _logger.LogWarning(ex, "Health check failed for QBittorrent client {clientId}", _downloadClientConfig.Id); + + return new HealthCheckResult + { + IsHealthy = false, + ErrorMessage = $"Connection failed: {ex.Message}", + ResponseTime = stopwatch.Elapsed + }; + } + } private async Task> GetTrackersAsync(string hash) { - if (_client == null) - { - throw new InvalidOperationException("QBittorrent client is not initialized"); - } - return (await _client.GetTorrentTrackersAsync(hash)) .Where(x => x.Url.Contains("**")) .ToList(); @@ -87,7 +109,6 @@ public partial class QBitService : DownloadService, IQBitService public override void Dispose() { - _client?.Dispose(); - _httpClient?.Dispose(); + _client.Dispose(); } } \ No newline at end of file diff --git a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs index c4474392..5d54f375 100644 --- a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs +++ b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs @@ -14,7 +14,7 @@ namespace Infrastructure.Verticals.DownloadClient.Transmission; public partial class TransmissionService : DownloadService, ITransmissionService { - private Client? _client; + private readonly Client _client; private static readonly string[] Fields = [ @@ -44,55 +44,61 @@ public partial class TransmissionService : DownloadService, ITransmissionService IHardLinkFileService hardLinkFileService, IDynamicHttpClientProvider httpClientProvider, EventPublisher eventPublisher, - BlocklistProvider blocklistProvider + BlocklistProvider blocklistProvider, + DownloadClientConfig downloadClientConfig ) : base( logger, cache, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService, - httpClientProvider, eventPublisher, blocklistProvider + httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig ) { - // Client will be initialized when Initialize() is called with a specific client configuration + UriBuilder uriBuilder = new(_downloadClientConfig.Url); + uriBuilder.Path = string.IsNullOrEmpty(_downloadClientConfig.UrlBase) + ? $"{uriBuilder.Path.TrimEnd('/')}/rpc" + : $"{uriBuilder.Path.TrimEnd('/')}/{_downloadClientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc"; + // TODO check if httpClientProvider creates a client as expected + _client = new Client( + _httpClient, + uriBuilder.Uri.ToString(), + login: _downloadClientConfig.Username, + password: _downloadClientConfig.Password + ); } - /// - public override void Initialize(DownloadClientConfig downloadClientConfig) - { - // Initialize base service first - base.Initialize(downloadClientConfig); - - // Ensure client type is correct - if (downloadClientConfig.TypeName != Common.Enums.DownloadClientTypeName.Transmission) - { - throw new InvalidOperationException($"Cannot initialize TransmissionService with client type {downloadClientConfig.TypeName}"); - } - - if (_httpClient == null) - { - throw new InvalidOperationException("HTTP client is not initialized"); - } - - // Create the RPC path - string rpcPath = string.IsNullOrEmpty(downloadClientConfig.UrlBase) - ? "/rpc" - : $"/{downloadClientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc"; - - // Create full RPC URL - string rpcUrl = new UriBuilder(downloadClientConfig.Url) { Path = rpcPath }.Uri.ToString(); - - // Create Transmission client - _client = new Client(_httpClient, rpcUrl, login: downloadClientConfig.Username, password: downloadClientConfig.Password); - - _logger.LogInformation("Initialized Transmission service for client {clientName} ({clientId})", - downloadClientConfig.Name, downloadClientConfig.Id); - } + // /// + // public override void Initialize(DownloadClientConfig downloadClientConfig) + // { + // // Initialize base service first + // base.Initialize(downloadClientConfig); + // + // // Ensure client type is correct + // if (downloadClientConfig.TypeName != Common.Enums.DownloadClientTypeName.Transmission) + // { + // throw new InvalidOperationException($"Cannot initialize TransmissionService with client type {downloadClientConfig.TypeName}"); + // } + // + // if (_httpClient == null) + // { + // throw new InvalidOperationException("HTTP client is not initialized"); + // } + // + // // Create the RPC path + // string rpcPath = string.IsNullOrEmpty(downloadClientConfig.UrlBase) + // ? "/rpc" + // : $"/{downloadClientConfig.UrlBase.TrimStart('/').TrimEnd('/')}/rpc"; + // + // // Create full RPC URL + // string rpcUrl = new UriBuilder(downloadClientConfig.Url) { Path = rpcPath }.Uri.ToString(); + // + // // Create Transmission client + // _client = new Client(_httpClient, rpcUrl, login: downloadClientConfig.Username, password: downloadClientConfig.Password); + // + // _logger.LogInformation("Initialized Transmission service for client {clientName} ({clientId})", + // downloadClientConfig.Name, downloadClientConfig.Id); + // } public override async Task LoginAsync() { - if (_client == null) - { - throw new InvalidOperationException("Transmission client is not initialized"); - } - try { await _client.GetSessionInformationAsync(); @@ -104,11 +110,78 @@ public partial class TransmissionService : DownloadService, ITransmissionService throw; } } + + public override async Task HealthCheckAsync() + { + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + try + { + bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) || + !string.IsNullOrEmpty(_downloadClientConfig.Password); + + if (hasCredentials) + { + // If credentials are provided, we must be able to authenticate for the service to be healthy + await _client.GetSessionInformationAsync(); + _logger.LogDebug("Health check: Successfully authenticated with Transmission client {clientId}", _downloadClientConfig.Id); + } + else + { + // If no credentials, test basic connectivity with a simple RPC call + // This will likely fail with authentication error, but that tells us the service is running + try + { + await _client.GetSessionInformationAsync(); + _logger.LogDebug("Health check: Successfully connected to Transmission client {clientId}", _downloadClientConfig.Id); + } + catch (Exception ex) when (ex.Message.Contains("401") || ex.Message.Contains("Unauthorized")) + { + // Authentication error means the service is running but requires credentials + _logger.LogDebug("Health check: Transmission client {clientId} is running but requires authentication", _downloadClientConfig.Id); + } + } + + stopwatch.Stop(); + + return new HealthCheckResult + { + IsHealthy = true, + ResponseTime = stopwatch.Elapsed + }; + } + catch (Exception ex) + { + stopwatch.Stop(); + + // Check if this is an authentication error when no credentials are provided + bool isAuthError = ex.Message.Contains("401") || ex.Message.Contains("Unauthorized"); + bool hasCredentials = !string.IsNullOrEmpty(_downloadClientConfig.Username) || + !string.IsNullOrEmpty(_downloadClientConfig.Password); + + if (isAuthError && !hasCredentials) + { + // Authentication error without credentials means service is running + return new HealthCheckResult + { + IsHealthy = true, + ResponseTime = stopwatch.Elapsed + }; + } + + _logger.LogWarning(ex, "Health check failed for Transmission client {clientId}", _downloadClientConfig.Id); + + return new HealthCheckResult + { + IsHealthy = false, + ErrorMessage = $"Connection failed: {ex.Message}", + ResponseTime = stopwatch.Elapsed + }; + } + } public override void Dispose() { - _client = null; - _httpClient?.Dispose(); } private async Task GetTorrentAsync(string hash)