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 Transmission.API.RPC; using Transmission.API.RPC.Arguments; using Transmission.API.RPC.Entity; namespace Infrastructure.Verticals.DownloadClient.Transmission; public sealed class TransmissionService : DownloadServiceBase { private readonly TransmissionConfig _config; private readonly Client _client; private TorrentInfo[]? _torrentsCache; public TransmissionService( IHttpClientFactory httpClientFactory, ILogger logger, IOptions config, IOptions queueCleanerConfig, FilenameEvaluator filenameEvaluator, Striker striker ) : base(logger, queueCleanerConfig, filenameEvaluator, striker) { _config = config.Value; _config.Validate(); _client = new( httpClientFactory.CreateClient(Constants.HttpClientWithRetryName), new Uri(_config.Url, "/transmission/rpc").ToString(), login: _config.Username, password: _config.Password ); } public override async Task LoginAsync() { await _client.GetSessionInformationAsync(); } public override async Task ShouldRemoveFromArrQueueAsync(string hash) { RemoveResult result = new(); TorrentInfo? torrent = await GetTorrentAsync(hash); if (torrent is null) { _logger.LogDebug("failed to find torrent {hash} in the download client", hash); return result; } bool shouldRemove = torrent.FileStats?.Length > 0; result.IsPrivate = torrent.IsPrivate ?? false; foreach (TransmissionTorrentFileStats? stats in torrent.FileStats ?? []) { if (!stats.Wanted.HasValue) { // if any files stats are missing, do not remove shouldRemove = false; } if (stats.Wanted.HasValue && stats.Wanted.Value) { // if any files are wanted, do not remove shouldRemove = false; } } // remove if all files are unwanted or download is stuck result.ShouldRemove = shouldRemove || IsItemStuckAndShouldRemove(torrent); return result; } public override async Task BlockUnwantedFilesAsync(string hash) { TorrentInfo? torrent = await GetTorrentAsync(hash); if (torrent?.FileStats is null || torrent.Files is null) { return; } if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false)) { // ignore private trackers _logger.LogDebug("skip files check | download is private | {name}", torrent.Name); return; } List unwantedFiles = []; for (int i = 0; i < torrent.Files.Length; i++) { if (torrent.FileStats?[i].Wanted == null) { continue; } if (!torrent.FileStats[i].Wanted.Value || _filenameEvaluator.IsValid(torrent.Files[i].Name)) { continue; } _logger.LogInformation("unwanted file found | {file}", torrent.Files[i].Name); unwantedFiles.Add(i); } if (unwantedFiles.Count is 0) { return; } _logger.LogDebug("changing priorities | torrent {hash}", hash); await _client.TorrentSetAsync(new TorrentSettings { Ids = [ torrent.Id ], FilesUnwanted = unwantedFiles.ToArray(), }); } public override void Dispose() { } private bool IsItemStuckAndShouldRemove(TorrentInfo torrent) { if (_queueCleanerConfig.StalledMaxStrikes is 0) { return false; } if (_queueCleanerConfig.StalledIgnorePrivate && (torrent.IsPrivate ?? false)) { // ignore private trackers _logger.LogDebug("skip stalled check | download is private | {name}", torrent.Name); return false; } if (torrent.Status is not 4) { // not in downloading state return false; } if (torrent.Eta > 0) { return false; } return StrikeAndCheckLimit(torrent.HashString!, torrent.Name!); } private async Task GetTorrentAsync(string hash) { TorrentInfo? torrent = _torrentsCache? .FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase)); if (_torrentsCache is null || torrent is null) { string[] fields = [ TorrentFields.FILES, TorrentFields.FILE_STATS, TorrentFields.HASH_STRING, TorrentFields.ID, TorrentFields.ETA, TorrentFields.NAME, TorrentFields.STATUS, TorrentFields.IS_PRIVATE ]; // refresh cache _torrentsCache = (await _client.TorrentGetAsync(fields)) ?.Torrents; } if (_torrentsCache?.Length is null or 0) { _logger.LogDebug("could not list torrents | {url}", _config.Url); } torrent = _torrentsCache?.FirstOrDefault(x => x.HashString.Equals(hash, StringComparison.InvariantCultureIgnoreCase)); if (torrent is null) { _logger.LogDebug("could not find torrent | {hash} | {url}", hash, _config.Url); } return torrent; } }