add content blocker (#5)

* refactored code
added deluge support
added transmission support
added content blocker
added blacklist and whitelist

* increased level on some logs; updated test docker compose; updated dev appsettings

* updated docker compose and readme

* moved some logs

* fixed env var typo; fixed sonarr and radarr default download client
This commit is contained in:
Marius Nechifor
2024-11-18 20:08:01 +02:00
committed by GitHub
parent b323cb40ae
commit e0a6c7842b
154 changed files with 4752 additions and 789 deletions
@@ -0,0 +1,138 @@
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using Common.Configuration;
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;
public DelugeClient(IOptions<DelugeConfig> config, IHttpClientFactory httpClientFactory)
{
_config = config.Value;
_httpClient = httpClientFactory.CreateClient(nameof(DelugeService));
}
public async Task<bool> LoginAsync()
{
return await SendRequest<bool>("auth.login", _config.Password);
}
public async Task<bool> Logout()
{
return await SendRequest<bool>("auth.delete_session");
}
public async Task<List<DelugeTorrent>> ListTorrents(Dictionary<string, string>? filters = null)
{
filters ??= new Dictionary<string, string>();
var keys = typeof(DelugeTorrent).GetAllJsonPropertyFromType();
Dictionary<string, DelugeTorrent> result =
await SendRequest<Dictionary<string, DelugeTorrent>>("core.get_torrents_status", filters, keys);
return result.Values.ToList();
}
public async Task<List<DelugeTorrentExtended>> ListTorrentsExtended(Dictionary<string, string>? filters = null)
{
filters ??= new Dictionary<string, string>();
var keys = typeof(DelugeTorrentExtended).GetAllJsonPropertyFromType();
Dictionary<string, DelugeTorrentExtended> result =
await SendRequest<Dictionary<string, DelugeTorrentExtended>>("core.get_torrents_status", filters, keys);
return result.Values.ToList();
}
public async Task<DelugeTorrent?> GetTorrent(string hash)
{
List<DelugeTorrent> torrents = await ListTorrents(new Dictionary<string, string>() { { "hash", hash } });
return torrents.FirstOrDefault();
}
public async Task<DelugeTorrentExtended?> GetTorrentExtended(string hash)
{
List<DelugeTorrentExtended> torrents =
await ListTorrentsExtended(new Dictionary<string, string> { { "hash", hash } });
return torrents.FirstOrDefault();
}
public async Task<DelugeContents?> GetTorrentFiles(string hash)
{
return await SendRequest<DelugeContents?>("web.get_torrent_files", hash);
}
public async Task ChangeFilesPriority(string hash, List<int> priorities)
{
Dictionary<string, List<int>> filePriorities = new()
{
{ "file_priorities", priorities }
};
await SendRequest<DelugeResponse<object>>("core.set_torrent_options", hash, filePriorities);
}
private async Task<String> PostJson(String json)
{
StringContent content = new StringContent(json);
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue("application/json");
var responseMessage = await _httpClient.PostAsync(new Uri(_config.Url, "/json"), content);
responseMessage.EnsureSuccessStatusCode();
var responseJson = await responseMessage.Content.ReadAsStringAsync();
return responseJson;
}
private 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<T> SendRequest<T>(string method, params object[] parameters)
{
return await SendRequest<T>(CreateRequest(method, parameters));
}
public async Task<T> SendRequest<T>(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<T>? webResponse = JsonConvert.DeserializeObject<DelugeResponse<T>>(responseJson, settings);
if (webResponse?.Error != null)
{
throw new DelugeClientException(webResponse.Error.Message);
}
if (webResponse?.ResponseId != webRequest.RequestId)
{
throw new DelugeClientException("desync");
}
return webResponse.Result;
}
}
@@ -0,0 +1,164 @@
using Common.Configuration;
using Domain.Models.Deluge.Response;
using Infrastructure.Verticals.ContentBlocker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Infrastructure.Verticals.DownloadClient.Deluge;
public sealed class DelugeService : IDownloadService
{
private readonly ILogger<DelugeService> _logger;
private readonly DelugeClient _client;
private readonly FilenameEvaluator _filenameEvaluator;
public DelugeService(
ILogger<DelugeService> logger,
IOptions<DelugeConfig> config,
IHttpClientFactory httpClientFactory,
FilenameEvaluator filenameEvaluator
)
{
_logger = logger;
_client = new (config, httpClientFactory);
_filenameEvaluator = filenameEvaluator;
}
public async Task LoginAsync()
{
await _client.LoginAsync();
}
public async Task<bool> ShouldRemoveFromArrQueueAsync(string hash)
{
hash = hash.ToLowerInvariant();
DelugeContents? contents = null;
if (!await HasMinimalStatus(hash))
{
return false;
}
try
{
contents = await _client.GetTorrentFiles(hash);
}
catch (Exception exception)
{
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
}
if (contents is null)
{
return false;
}
bool shouldRemove = true;
ProcessFiles(contents.Contents, (_, file) =>
{
if (file.Priority > 0)
{
shouldRemove = false;
}
});
return shouldRemove;
}
public async Task BlockUnwantedFilesAsync(string hash)
{
hash = hash.ToLowerInvariant();
if (!await HasMinimalStatus(hash))
{
return;
}
DelugeContents? contents = null;
try
{
contents = await _client.GetTorrentFiles(hash);
}
catch (Exception exception)
{
_logger.LogDebug(exception, "failed to find torrent {hash} in the download client", hash);
}
if (contents is null)
{
return;
}
Dictionary<int, int> priorities = [];
bool hasPriorityUpdates = false;
ProcessFiles(contents.Contents, (name, file) =>
{
int priority = file.Priority;
if (file.Priority is not 0 && !_filenameEvaluator.IsValid(name))
{
priority = 0;
hasPriorityUpdates = true;
_logger.LogInformation("unwanted file found | {file}", file.Path);
}
priorities.Add(file.Index, priority);
});
if (!hasPriorityUpdates)
{
return;
}
_logger.LogDebug("changing priorities | torrent {hash}", hash);
List<int> sortedPriorities = priorities
.OrderBy(x => x.Key)
.Select(x => x.Value)
.ToList();
await _client.ChangeFilesPriority(hash, sortedPriorities);
}
private async Task<bool> HasMinimalStatus(string hash)
{
DelugeMinimalStatus? status = await _client.SendRequest<DelugeMinimalStatus?>(
"web.get_torrent_status",
hash,
new[] { "hash" }
);
if (status?.Hash is null)
{
_logger.LogDebug("failed to find torrent {hash} in the download client", hash);
return false;
}
return true;
}
private static void ProcessFiles(Dictionary<string, DelugeFileOrDirectory> contents, Action<string, DelugeFileOrDirectory> processFile)
{
foreach (var (name, data) in contents)
{
switch (data.Type)
{
case "file":
processFile(name, data);
break;
case "dir" when data.Contents is not null:
// Recurse into subdirectories
ProcessFiles(data.Contents, processFile);
break;
}
}
}
public void Dispose()
{
}
}
@@ -0,0 +1,20 @@
using Newtonsoft.Json;
namespace Infrastructure.Verticals.DownloadClient.Deluge.Extensions;
internal static class DelugeExtensions
{
public static List<String?> GetAllJsonPropertyFromType(this Type t)
{
var type = typeof(JsonPropertyAttribute);
var props = t.GetProperties()
.Where(prop => Attribute.IsDefined(prop, type))
.ToList();
return props
.Select(x => x.GetCustomAttributes(type, true).Single())
.Cast<JsonPropertyAttribute>()
.Select(x => x.PropertyName)
.ToList();
}
}