mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-11-02 12:25:07 -05:00
214 lines
8.0 KiB
C#
214 lines
8.0 KiB
C#
using System.Net.Http.Headers;
|
|
using Cleanuparr.Domain.Entities.HealthCheck;
|
|
using Cleanuparr.Infrastructure.Events;
|
|
using Cleanuparr.Infrastructure.Events.Interfaces;
|
|
using Cleanuparr.Infrastructure.Features.Files;
|
|
using Cleanuparr.Infrastructure.Features.ItemStriker;
|
|
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
|
|
using Cleanuparr.Infrastructure.Helpers;
|
|
using Cleanuparr.Infrastructure.Http;
|
|
using Cleanuparr.Infrastructure.Interceptors;
|
|
using Cleanuparr.Infrastructure.Services;
|
|
using Cleanuparr.Infrastructure.Services.Interfaces;
|
|
using Cleanuparr.Persistence.Models.Configuration;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.Extensions.Logging;
|
|
using Newtonsoft.Json.Linq;
|
|
using QBittorrent.Client;
|
|
|
|
namespace Cleanuparr.Infrastructure.Features.DownloadClient.QBittorrent;
|
|
|
|
public partial class QBitService : TorrentDownloadService, IDeadTorrentCapable, IErrorRecoveryCapable
|
|
{
|
|
protected readonly IQBittorrentClientWrapper _client;
|
|
private bool? _altSpeedLimitActive;
|
|
|
|
public QBitService(
|
|
ILogger<QBitService> logger,
|
|
IFilenameEvaluator filenameEvaluator,
|
|
IStriker striker,
|
|
IDryRunInterceptor dryRunInterceptor,
|
|
IHardLinkFileService hardLinkFileService,
|
|
IDynamicHttpClientProvider httpClientProvider,
|
|
IEventPublisher eventPublisher,
|
|
IBlocklistProvider blocklistProvider,
|
|
DownloadClientConfig downloadClientConfig,
|
|
IQueueRuleEvaluator queueRuleEvaluator,
|
|
ISeedingRuleEvaluator seedingRuleEvaluator,
|
|
TimeProvider timeProvider
|
|
) : base(
|
|
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
|
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator, timeProvider
|
|
)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(downloadClientConfig.ApiKey))
|
|
{
|
|
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", downloadClientConfig.ApiKey);
|
|
}
|
|
|
|
var qBittorrentClient = new QBittorrentClient(_httpClient, downloadClientConfig.Url);
|
|
_client = new QBittorrentClientWrapper(qBittorrentClient);
|
|
}
|
|
|
|
// Internal constructor for testing
|
|
internal QBitService(
|
|
ILogger<QBitService> logger,
|
|
IFilenameEvaluator filenameEvaluator,
|
|
IStriker striker,
|
|
IDryRunInterceptor dryRunInterceptor,
|
|
IHardLinkFileService hardLinkFileService,
|
|
IDynamicHttpClientProvider httpClientProvider,
|
|
IEventPublisher eventPublisher,
|
|
IBlocklistProvider blocklistProvider,
|
|
DownloadClientConfig downloadClientConfig,
|
|
IQueueRuleEvaluator queueRuleEvaluator,
|
|
ISeedingRuleEvaluator seedingRuleEvaluator,
|
|
TimeProvider timeProvider,
|
|
IQBittorrentClientWrapper clientWrapper
|
|
) : base(
|
|
logger, filenameEvaluator, striker, dryRunInterceptor, hardLinkFileService,
|
|
httpClientProvider, eventPublisher, blocklistProvider, downloadClientConfig, queueRuleEvaluator, seedingRuleEvaluator, timeProvider
|
|
)
|
|
{
|
|
_client = clientWrapper;
|
|
}
|
|
|
|
public override async Task LoginAsync()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(_downloadClientConfig.ApiKey))
|
|
{
|
|
_logger.LogDebug("API key configured for client {ClientId}, skipping login", _downloadClientConfig.Id);
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(_downloadClientConfig.Username) && string.IsNullOrEmpty(_downloadClientConfig.Password))
|
|
{
|
|
_logger.LogDebug("No credentials configured for client {ClientId}, skipping login", _downloadClientConfig.Id);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
await _client.LoginAsync(_downloadClientConfig.Username, _downloadClientConfig.Password);
|
|
_logger.LogDebug("Successfully logged in to qBittorrent client {ClientId}", _downloadClientConfig.Id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to login to qBittorrent client {ClientId}", _downloadClientConfig.Id);
|
|
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 (!string.IsNullOrWhiteSpace(_downloadClientConfig.ApiKey))
|
|
{
|
|
// login with an API key returns 403, so probe an auth-protected endpoint
|
|
await _client.GetAlternativeSpeedLimitsEnabledAsync();
|
|
_logger.LogDebug("Health check: Successfully authenticated to qBittorrent client {ClientId}", _downloadClientConfig.Id);
|
|
}
|
|
else if (hasCredentials)
|
|
{
|
|
// If credentials are provided, we must be able to log in 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
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Syncs blacklist patterns from configured file to qBittorrent excluded file names
|
|
/// </summary>
|
|
/// <param name="excludedFileNames">List of excluded file names for qBittorrent</param>
|
|
public async Task UpdateBlacklistAsync(string excludedFileNames)
|
|
{
|
|
Preferences preferences = new()
|
|
{
|
|
AdditionalData = new Dictionary<string, JToken>
|
|
{
|
|
{ "excluded_file_names", excludedFileNames }
|
|
}
|
|
};
|
|
|
|
await _client.SetPreferencesAsync(preferences);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the real trackers of a torrent, dropping the "** [DHT] **" style pseudo-rows.
|
|
/// </summary>
|
|
/// <returns>
|
|
/// An empty list for a trackerless torrent, or null when qBittorrent no longer knows the hash.
|
|
/// </returns>
|
|
private async Task<IReadOnlyList<TorrentTracker>?> GetTrackersAsync(string hash)
|
|
{
|
|
IReadOnlyList<TorrentTracker>? trackers = await _client.GetTorrentTrackersAsync(hash);
|
|
|
|
return trackers
|
|
?.Where(x => x.Url?.Contains("**") is not true)
|
|
.ToList();
|
|
}
|
|
|
|
protected override async Task<bool> IsAltSpeedLimitActiveAsync()
|
|
{
|
|
if (_altSpeedLimitActive is not null)
|
|
{
|
|
return _altSpeedLimitActive.Value;
|
|
}
|
|
|
|
try
|
|
{
|
|
_altSpeedLimitActive = await _client.GetAlternativeSpeedLimitsEnabledAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to read alternate speed limits state from qBittorrent client {Name}", _downloadClientConfig.Name);
|
|
throw;
|
|
}
|
|
|
|
return _altSpeedLimitActive.Value;
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_client.Dispose();
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
} |