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:
@@ -0,0 +1,124 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using Common.Configuration.ContentBlocker;
|
||||
using Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class BlocklistProvider
|
||||
{
|
||||
private readonly ILogger<BlocklistProvider> _logger;
|
||||
private readonly ContentBlockerConfig _config;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
public BlocklistType BlocklistType { get; }
|
||||
|
||||
public List<string> Patterns { get; } = [];
|
||||
|
||||
public List<Regex> Regexes { get; } = [];
|
||||
|
||||
public BlocklistProvider(
|
||||
ILogger<BlocklistProvider> logger,
|
||||
IOptions<ContentBlockerConfig> config,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Value;
|
||||
_httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
_config.Validate();
|
||||
|
||||
if (_config.Blacklist?.Enabled is true)
|
||||
{
|
||||
BlocklistType = BlocklistType.Blacklist;
|
||||
}
|
||||
|
||||
if (_config.Whitelist?.Enabled is true)
|
||||
{
|
||||
BlocklistType = BlocklistType.Whitelist;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LoadBlocklistAsync()
|
||||
{
|
||||
if (Patterns.Count > 0 || Regexes.Count > 0)
|
||||
{
|
||||
_logger.LogDebug("blocklist already loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await LoadPatternsAndRegexesAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogError("failed to load {type}", BlocklistType.ToString());
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPatternsAndRegexesAsync()
|
||||
{
|
||||
string[] patterns;
|
||||
|
||||
if (BlocklistType is BlocklistType.Blacklist)
|
||||
{
|
||||
patterns = await ReadContentAsync(_config.Blacklist.Path);
|
||||
}
|
||||
else
|
||||
{
|
||||
patterns = await ReadContentAsync(_config.Whitelist.Path);
|
||||
}
|
||||
|
||||
long startTime = Stopwatch.GetTimestamp();
|
||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||
|
||||
Parallel.ForEach(patterns, options, pattern =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Regex regex = new(pattern, RegexOptions.Compiled);
|
||||
Regexes.Add(regex);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
Patterns.Add(pattern);
|
||||
}
|
||||
});
|
||||
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
|
||||
_logger.LogDebug("loaded {count} patterns", Patterns.Count);
|
||||
_logger.LogDebug("loaded {count} regexes", Regexes.Count);
|
||||
_logger.LogDebug("blocklist loaded in {elapsed} ms", elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
private async Task<string[]> ReadContentAsync(string path)
|
||||
{
|
||||
if (Uri.TryCreate(path, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
{
|
||||
// http(s) url
|
||||
return await ReadFromUrlAsync(path);
|
||||
}
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
// local file path
|
||||
return await File.ReadAllLinesAsync(path);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"blocklist not found | {path}");
|
||||
}
|
||||
|
||||
private async Task<string[]> ReadFromUrlAsync(string url)
|
||||
{
|
||||
using HttpResponseMessage response = await _httpClient.GetAsync(url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
return (await response.Content.ReadAsStringAsync())
|
||||
.Split(['\r','\n'], StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using Common.Configuration;
|
||||
using Domain.Arr.Queue;
|
||||
using Domain.Enums;
|
||||
using Infrastructure.Verticals.Arr;
|
||||
using Infrastructure.Verticals.DownloadClient;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class ContentBlocker : IDisposable
|
||||
{
|
||||
private readonly ILogger<ContentBlocker> _logger;
|
||||
private readonly SonarrConfig _sonarrConfig;
|
||||
private readonly RadarrConfig _radarrConfig;
|
||||
private readonly SonarrClient _sonarrClient;
|
||||
private readonly RadarrClient _radarrClient;
|
||||
private readonly ArrQueueIterator _arrArrQueueIterator;
|
||||
private readonly BlocklistProvider _blocklistProvider;
|
||||
private readonly IDownloadService _downloadService;
|
||||
|
||||
public ContentBlocker(
|
||||
ILogger<ContentBlocker> logger,
|
||||
IOptions<SonarrConfig> sonarrConfig,
|
||||
IOptions<RadarrConfig> radarrConfig,
|
||||
SonarrClient sonarrClient,
|
||||
RadarrClient radarrClient,
|
||||
ArrQueueIterator arrArrQueueIterator,
|
||||
BlocklistProvider blocklistProvider,
|
||||
DownloadServiceFactory downloadServiceFactory
|
||||
)
|
||||
{
|
||||
_logger = logger;
|
||||
_sonarrConfig = sonarrConfig.Value;
|
||||
_radarrConfig = radarrConfig.Value;
|
||||
_sonarrClient = sonarrClient;
|
||||
_radarrClient = radarrClient;
|
||||
_arrArrQueueIterator = arrArrQueueIterator;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
_downloadService = downloadServiceFactory.CreateDownloadClient();
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
await _blocklistProvider.LoadBlocklistAsync();
|
||||
await _downloadService.LoginAsync();
|
||||
|
||||
await ProcessArrConfigAsync(_sonarrConfig, InstanceType.Sonarr);
|
||||
await ProcessArrConfigAsync(_radarrConfig, InstanceType.Radarr);
|
||||
}
|
||||
|
||||
private async Task ProcessArrConfigAsync(ArrConfig config, InstanceType instanceType)
|
||||
{
|
||||
if (!config.Enabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (ArrInstance arrInstance in config.Instances)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessInstanceAsync(arrInstance, instanceType);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "failed to block content for {type} instance | {url}", instanceType, arrInstance.Url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType)
|
||||
{
|
||||
ArrClient arrClient = GetClient(instanceType);
|
||||
|
||||
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
|
||||
{
|
||||
foreach (QueueRecord record in items)
|
||||
{
|
||||
_logger.LogDebug("searching unwanted files for {title}", record.Title);
|
||||
await _downloadService.BlockUnwantedFilesAsync(record.DownloadId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ArrClient GetClient(InstanceType type) =>
|
||||
type switch
|
||||
{
|
||||
InstanceType.Sonarr => _sonarrClient,
|
||||
InstanceType.Radarr => _radarrClient,
|
||||
_ => throw new NotImplementedException($"instance type {type} is not yet supported")
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_downloadService.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using Domain.Enums;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Infrastructure.Verticals.ContentBlocker;
|
||||
|
||||
public sealed class FilenameEvaluator
|
||||
{
|
||||
private readonly ILogger<FilenameEvaluator> _logger;
|
||||
private readonly BlocklistProvider _blocklistProvider;
|
||||
|
||||
public FilenameEvaluator(ILogger<FilenameEvaluator> logger, BlocklistProvider blocklistProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_blocklistProvider = blocklistProvider;
|
||||
}
|
||||
|
||||
// TODO create unit tests
|
||||
public bool IsValid(string filename)
|
||||
{
|
||||
return IsValidAgainstPatterns(filename) && IsValidAgainstRegexes(filename);
|
||||
}
|
||||
|
||||
private bool IsValidAgainstPatterns(string filename)
|
||||
{
|
||||
if (_blocklistProvider.Patterns.Count is 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _blocklistProvider.BlocklistType switch
|
||||
{
|
||||
BlocklistType.Blacklist => !_blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||
BlocklistType.Whitelist => _blocklistProvider.Patterns.Any(pattern => MatchesPattern(filename, pattern)),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private bool IsValidAgainstRegexes(string filename)
|
||||
{
|
||||
if (_blocklistProvider.Regexes.Count is 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return _blocklistProvider.BlocklistType switch
|
||||
{
|
||||
BlocklistType.Blacklist => !_blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
||||
BlocklistType.Whitelist => _blocklistProvider.Regexes.Any(regex => regex.IsMatch(filename)),
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
private static bool MatchesPattern(string filename, string pattern)
|
||||
{
|
||||
bool hasStartWildcard = pattern.StartsWith('*');
|
||||
bool hasEndWildcard = pattern.EndsWith('*');
|
||||
|
||||
if (hasStartWildcard && hasEndWildcard)
|
||||
{
|
||||
return filename.Contains(
|
||||
pattern.Substring(1, pattern.Length - 2),
|
||||
StringComparison.InvariantCultureIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
if (hasStartWildcard)
|
||||
{
|
||||
return filename.EndsWith(pattern.Substring(1), StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
if (hasEndWildcard)
|
||||
{
|
||||
return filename.StartsWith(
|
||||
pattern.Substring(0, pattern.Length - 1),
|
||||
StringComparison.InvariantCultureIgnoreCase
|
||||
);
|
||||
}
|
||||
|
||||
return filename == pattern;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user