using System.Net.Http.Headers; using System.Text.Json.Serialization; using Common.Configuration; using Common.Configuration.DownloadClient; using Domain.Models.Deluge.Exceptions; using Domain.Models.Deluge.Request; using Domain.Models.Deluge.Response; using Infrastructure.Verticals.DownloadClient.Deluge.Extensions; using Microsoft.Extensions.Options; using Newtonsoft.Json; namespace Infrastructure.Verticals.DownloadClient.Deluge; public sealed class DelugeClient { private readonly DelugeConfig _config; private readonly HttpClient _httpClient; private static readonly IReadOnlyList Fields = [ "hash", "state", "name", "eta", "private", "total_done", "label", "seeding_time", "ratio", "trackers", "download_location" ]; public DelugeClient(IOptions config, IHttpClientFactory httpClientFactory) { _config = config.Value; _config.Validate(); _httpClient = httpClientFactory.CreateClient(nameof(DelugeService)); } public async Task LoginAsync() { return await SendRequest("auth.login", _config.Password); } public async Task ListMethodsAsync() { await SendRequest("system.listMethods"); } public async Task Logout() { return await SendRequest("auth.delete_session"); } public async Task> ListTorrents(Dictionary? filters = null) { filters ??= new Dictionary(); var keys = typeof(DelugeTorrent).GetAllJsonPropertyFromType(); Dictionary result = await SendRequest>("core.get_torrents_status", filters, keys); return result.Values.ToList(); } public async Task> ListTorrentsExtended(Dictionary? filters = null) { filters ??= new Dictionary(); var keys = typeof(DelugeTorrentExtended).GetAllJsonPropertyFromType(); Dictionary result = await SendRequest>("core.get_torrents_status", filters, keys); return result.Values.ToList(); } public async Task GetTorrent(string hash) { List torrents = await ListTorrents(new Dictionary() { { "hash", hash } }); return torrents.FirstOrDefault(); } public async Task GetTorrentExtended(string hash) { List torrents = await ListTorrentsExtended(new Dictionary { { "hash", hash } }); return torrents.FirstOrDefault(); } public async Task GetTorrentStatus(string hash) { try { return await SendRequest( "web.get_torrent_status", hash, Fields ); } catch (DelugeClientException e) { // Deluge returns an error when the torrent is not found if (e.Message == "AttributeError: 'NoneType' object has no attribute 'call'") { return null; } throw; } } public async Task?> GetStatusForAllTorrents() { Dictionary? downloads = await SendRequest?>( "core.get_torrents_status", "", Fields ); return downloads?.Values.ToList(); } public async Task GetTorrentFiles(string hash) { return await SendRequest("web.get_torrent_files", hash); } public async Task ChangeFilesPriority(string hash, List priorities) { Dictionary> filePriorities = new() { { "file_priorities", priorities } }; await SendRequest>("core.set_torrent_options", hash, filePriorities); } public async Task DeleteTorrents(List hashes) { await SendRequest>("core.remove_torrents", hashes, true); } private async Task PostJson(String json) { StringContent content = new StringContent(json); content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json"); UriBuilder uriBuilder = new(_config.Url); uriBuilder.Path = string.IsNullOrEmpty(_config.UrlBase) ? $"{uriBuilder.Path.TrimEnd('/')}/json" : $"{uriBuilder.Path.TrimEnd('/')}/{_config.UrlBase.TrimStart('/').TrimEnd('/')}/json"; var responseMessage = await _httpClient.PostAsync(uriBuilder.Uri, content); responseMessage.EnsureSuccessStatusCode(); var responseJson = await responseMessage.Content.ReadAsStringAsync(); return responseJson; } private static DelugeRequest CreateRequest(string method, params object[] parameters) { if (String.IsNullOrWhiteSpace(method)) { throw new ArgumentException(nameof(method)); } return new DelugeRequest(1, method, parameters); } public async Task SendRequest(string method, params object[] parameters) { return await SendRequest(CreateRequest(method, parameters)); } public async Task SendRequest(DelugeRequest webRequest) { var requestJson = JsonConvert.SerializeObject(webRequest, Formatting.None, new JsonSerializerSettings { NullValueHandling = webRequest.NullValueHandling }); var responseJson = await PostJson(requestJson); var settings = new JsonSerializerSettings { Error = (_, args) => { // Suppress the error and continue args.ErrorContext.Handled = true; } }; DelugeResponse? webResponse = JsonConvert.DeserializeObject>(responseJson, settings); if (webResponse?.Error != null) { throw new DelugeClientException(webResponse.Error.Message); } if (webResponse?.ResponseId != webRequest.RequestId) { throw new DelugeClientException("desync"); } return webResponse.Result; } public async Task> GetLabels() { return await SendRequest>("label.get_labels"); } public async Task CreateLabel(string label) { await SendRequest>("label.add", label); } public async Task SetTorrentLabel(string hash, string newLabel) { await SendRequest>("label.set_torrent", hash, newLabel); } }