fixed health checks and download service factory

This commit is contained in:
Flaminel
2025-06-16 20:17:06 +03:00
parent b4548573ee
commit f651663fd3
8 changed files with 352 additions and 198 deletions

View File

@@ -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<Guid, HealthStatus>();

View File

@@ -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);

View File

@@ -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<DelugeService> 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
}
/// <inheritdoc />
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<HealthCheckResult> 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<string, DelugeFileOrDirectory>? contents, Action<string, DelugeFileOrDirectory> processFile)
{
if (contents is null)
@@ -110,7 +142,5 @@ public partial class DelugeService : DownloadService, IDelugeService
public override void Dispose()
{
_client = null;
_httpClient?.Dispose();
}
}

View File

@@ -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<DownloadService> _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<DownloadService> 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);
}
/// <inheritdoc />
@@ -70,22 +73,24 @@ public abstract class DownloadService : IDownloadService
return _downloadClientConfig.Id;
}
/// <inheritdoc />
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);
}
// /// <inheritdoc />
// 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<HealthCheckResult> HealthCheckAsync();
public abstract Task<DownloadCheckResult> ShouldRemoveFromArrQueueAsync(string hash,
IReadOnlyList<string> ignoredDownloads);

View File

@@ -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<QBitService>(downloadClientConfig),
DownloadClientTypeName.Deluge => CreateClientService<DelugeService>(downloadClientConfig),
DownloadClientTypeName.Transmission => CreateClientService<TransmissionService>(downloadClientConfig),
DownloadClientTypeName.QBittorrent => CreateQBitService(downloadClientConfig),
DownloadClientTypeName.Deluge => CreateDelugeService(downloadClientConfig),
DownloadClientTypeName.Transmission => CreateTransmissionService(downloadClientConfig),
_ => throw new NotSupportedException($"Download client type {downloadClientConfig.TypeName} is not supported")
};
}
/// <summary>
/// Creates a download client service for a specific client type
/// </summary>
/// <typeparam name="T">The type of download service to create</typeparam>
/// <param name="downloadClientConfig">The client configuration</param>
/// <returns>An implementation of IDownloadService</returns>
private T CreateClientService<T>(DownloadClientConfig downloadClientConfig) where T : IDownloadService
private QBitService CreateQBitService(DownloadClientConfig downloadClientConfig)
{
var service = _serviceProvider.GetRequiredService<T>();
service.Initialize(downloadClientConfig);
var logger = _serviceProvider.GetRequiredService<ILogger<QBitService>>();
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
var striker = _serviceProvider.GetRequiredService<IStriker>();
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
var hardLinkFileService = _serviceProvider.GetRequiredService<IHardLinkFileService>();
var httpClientProvider = _serviceProvider.GetRequiredService<IDynamicHttpClientProvider>();
var eventPublisher = _serviceProvider.GetRequiredService<EventPublisher>();
var blocklistProvider = _serviceProvider.GetRequiredService<BlocklistProvider>();
// 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<ILogger<DelugeService>>();
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
var striker = _serviceProvider.GetRequiredService<IStriker>();
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
var hardLinkFileService = _serviceProvider.GetRequiredService<IHardLinkFileService>();
var httpClientProvider = _serviceProvider.GetRequiredService<IDynamicHttpClientProvider>();
var eventPublisher = _serviceProvider.GetRequiredService<EventPublisher>();
var blocklistProvider = _serviceProvider.GetRequiredService<BlocklistProvider>();
// 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<ILogger<TransmissionService>>();
var cache = _serviceProvider.GetRequiredService<IMemoryCache>();
var filenameEvaluator = _serviceProvider.GetRequiredService<IFilenameEvaluator>();
var striker = _serviceProvider.GetRequiredService<IStriker>();
var dryRunInterceptor = _serviceProvider.GetRequiredService<IDryRunInterceptor>();
var hardLinkFileService = _serviceProvider.GetRequiredService<IHardLinkFileService>();
var httpClientProvider = _serviceProvider.GetRequiredService<IDynamicHttpClientProvider>();
var eventPublisher = _serviceProvider.GetRequiredService<EventPublisher>();
var blocklistProvider = _serviceProvider.GetRequiredService<BlocklistProvider>();
// Create the TransmissionService instance
TransmissionService service = new(
logger, cache, filenameEvaluator, striker, dryRunInterceptor,
hardLinkFileService, httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig
);
return service;
}
}

View File

@@ -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
/// <returns>The client ID</returns>
Guid GetClientId();
/// <summary>
/// Initializes the download service with client-specific configuration
/// </summary>
/// <param name="downloadClientConfig">The client configuration</param>
public void Initialize(DownloadClientConfig downloadClientConfig);
public Task LoginAsync();
/// <summary>
/// Performs a health check on the download client
/// </summary>
/// <returns>The health check result</returns>
public Task<HealthCheckResult> HealthCheckAsync();
/// <summary>
/// Checks whether the download should be removed from the *arr queue.
/// </summary>

View File

@@ -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<QBitService> 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);
}
/// <inheritdoc />
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<HealthCheckResult> 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<IReadOnlyList<TorrentTracker>> 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();
}
}

View File

@@ -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
);
}
/// <inheritdoc />
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);
}
// /// <inheritdoc />
// 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<HealthCheckResult> 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<TorrentInfo?> GetTorrentAsync(string hash)