mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-01-18 02:38:20 -05:00
fixed health checks and download service factory
This commit is contained in:
@@ -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>();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user