using System.Collections.Concurrent; using System.Text.RegularExpressions; using Common.Configuration.ContentBlocker; using Common.Configuration.DownloadClient; using Common.Configuration.QueueCleaner; using Common.Helpers; using Infrastructure.Verticals.ContentBlocker; using Infrastructure.Verticals.ItemStriker; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using QBittorrent.Client; namespace Infrastructure.Verticals.DownloadClient.QBittorrent; public sealed class QBitService : DownloadServiceBase { private readonly QBitConfig _config; private readonly QBittorrentClient _client; public QBitService( ILogger logger, IHttpClientFactory httpClientFactory, IOptions config, IOptions queueCleanerConfig, FilenameEvaluator filenameEvaluator, Striker striker ) : base(logger, queueCleanerConfig, filenameEvaluator, striker) { _config = config.Value; _config.Validate(); _client = new(httpClientFactory.CreateClient(Constants.HttpClientWithRetryName), _config.Url); } public override async Task LoginAsync() { if (string.IsNullOrEmpty(_config.Username) && string.IsNullOrEmpty(_config.Password)) { return; } await _client.LoginAsync(_config.Username, _config.Password); } /// public override async Task ShouldRemoveFromArrQueueAsync(string hash) { RemoveResult result = new(); TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] })) .FirstOrDefault(); if (torrent is null) { _logger.LogDebug("failed to find torrent {hash} in the download client", hash); return result; } TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash); if (torrentProperties is null) { _logger.LogDebug("failed to find torrent properties {hash} in the download client", hash); return result; } result.IsPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) && bool.TryParse(dictValue?.ToString(), out bool boolValue) && boolValue; // if all files were blocked by qBittorrent if (torrent is { CompletionOn: not null, Downloaded: null or 0 }) { result.ShouldRemove = true; return result; } IReadOnlyList? files = await _client.GetTorrentContentsAsync(hash); // if all files are marked as skip if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip)) { result.ShouldRemove = true; return result; } result.ShouldRemove = IsItemStuckAndShouldRemove(torrent, result.IsPrivate); return result; } /// public override async Task BlockUnwantedFilesAsync( string hash, BlocklistType blocklistType, ConcurrentBag patterns, ConcurrentBag regexes ) { TorrentInfo? torrent = (await _client.GetTorrentListAsync(new TorrentListQuery { Hashes = [hash] })) .FirstOrDefault(); if (torrent is null) { _logger.LogDebug("failed to find torrent {hash} in the download client", hash); return false; } TorrentProperties? torrentProperties = await _client.GetTorrentPropertiesAsync(hash); if (torrentProperties is null) { _logger.LogDebug("failed to find torrent properties {hash} in the download client", hash); return false; } bool isPrivate = torrentProperties.AdditionalData.TryGetValue("is_private", out var dictValue) && bool.TryParse(dictValue?.ToString(), out bool boolValue) && boolValue; if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate) { // ignore private trackers _logger.LogDebug("skip files check | download is private | {name}", torrent.Name); return false; } IReadOnlyList? files = await _client.GetTorrentContentsAsync(hash); if (files is null) { return false; } List unwantedFiles = []; long totalFiles = 0; long totalUnwantedFiles = 0; foreach (TorrentContent file in files) { if (!file.Index.HasValue) { continue; } totalFiles++; if (file.Priority is TorrentContentPriority.Skip) { totalUnwantedFiles++; continue; } if (_filenameEvaluator.IsValid(file.Name, blocklistType, patterns, regexes)) { continue; } _logger.LogInformation("unwanted file found | {file}", file.Name); unwantedFiles.Add(file.Index.Value); totalUnwantedFiles++; } if (unwantedFiles.Count is 0) { return false; } if (totalUnwantedFiles == totalFiles) { // Skip marking files as unwanted. The download will be removed completely. return true; } foreach (int fileIndex in unwantedFiles) { await _client.SetFilePriorityAsync(hash, fileIndex, TorrentContentPriority.Skip); } return false; } /// public override async Task Delete(string hash) { await _client.DeleteAsync(hash, deleteDownloadedData: true); } public override void Dispose() { _client.Dispose(); } private bool IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate) { if (_queueCleanerConfig.StalledMaxStrikes is 0) { return false; } if (_queueCleanerConfig.StalledIgnorePrivate && isPrivate) { // ignore private trackers _logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name); return false; } if (torrent.State is not TorrentState.StalledDownload and not TorrentState.FetchingMetadata and not TorrentState.ForcedFetchingMetadata) { // ignore other states return false; } return StrikeAndCheckLimit(torrent.Hash, torrent.Name); } }