Finish rebranding Content Blocker to Malware Blocker (#271)

This commit is contained in:
Flaminel
2025-08-06 22:55:39 +03:00
committed by GitHub
parent cad1b51202
commit 3b0275c411
39 changed files with 246 additions and 370 deletions

View File

@@ -15,7 +15,8 @@ Cleanuparr was created primarily to address malicious files, such as `*.lnk` or
> - Remove and block downloads that are **failing to be imported** by the arrs.
> - Remove and block downloads that are **stalled** or in **metadata downloading** state.
> - Remove and block downloads that have a **low download speed** or **high estimated completion time**.
> - Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Content Blocker**.
> - Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Malware Blocker**.
> - Remove and block known malware based on patterns found by the community.
> - Automatically trigger a search for downloads removed from the arrs.
> - Clean up downloads that have been **seeding** for a certain amount of time.
> - Remove downloads that are **orphaned**/have no **hardlinks**/are not referenced by the arrs anymore (with [cross-seed](https://www.cross-seed.org/) support).

View File

@@ -11,9 +11,9 @@ using Cleanuparr.Infrastructure.Utilities;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Mapster;
@@ -65,8 +65,8 @@ public class ConfigurationController : ControllerBase
}
}
[HttpGet("content_blocker")]
public async Task<IActionResult> GetContentBlockerConfig()
[HttpGet("malware_blocker")]
public async Task<IActionResult> GetMalwareBlockerConfig()
{
await DataContext.Lock.WaitAsync();
try
@@ -483,8 +483,8 @@ public class ConfigurationController : ControllerBase
}
}
[HttpPut("content_blocker")]
public async Task<IActionResult> UpdateContentBlockerConfig([FromBody] ContentBlockerConfig newConfig)
[HttpPut("malware_blocker")]
public async Task<IActionResult> UpdateMalwareBlockerConfig([FromBody] ContentBlockerConfig newConfig)
{
await DataContext.Lock.WaitAsync();
try
@@ -515,11 +515,11 @@ public class ConfigurationController : ControllerBase
// Update the scheduler based on configuration changes
await UpdateJobSchedule(oldConfig, JobType.MalwareBlocker);
return Ok(new { Message = "ContentBlocker configuration updated successfully" });
return Ok(new { Message = "MalwareBlocker configuration updated successfully" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save ContentBlocker configuration");
_logger.LogError(ex, "Failed to save MalwareBlocker configuration");
throw;
}
finally

View File

@@ -1,9 +1,8 @@
using Cleanuparr.Application.Features.ContentBlocker;
using Cleanuparr.Application.Features.DownloadCleaner;
using Cleanuparr.Application.Features.MalwareBlocker;
using Cleanuparr.Application.Features.QueueCleaner;
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.Arr;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.DownloadClient;
using Cleanuparr.Infrastructure.Features.DownloadHunter;
using Cleanuparr.Infrastructure.Features.DownloadHunter.Interfaces;
@@ -11,6 +10,7 @@ using Cleanuparr.Infrastructure.Features.DownloadRemover;
using Cleanuparr.Infrastructure.Features.DownloadRemover.Interfaces;
using Cleanuparr.Infrastructure.Features.Files;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Features.Security;
using Cleanuparr.Infrastructure.Interceptors;
using Cleanuparr.Infrastructure.Services;

View File

@@ -1,12 +1,12 @@
using Cleanuparr.Application.Features.ContentBlocker;
using Cleanuparr.Application.Features.DownloadCleaner;
using Cleanuparr.Application.Features.MalwareBlocker;
using Cleanuparr.Application.Features.QueueCleaner;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Features.Jobs;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Cleanuparr.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
@@ -94,7 +94,7 @@ public class BackgroundJobManager : IHostedService
QueueCleanerConfig queueCleanerConfig = await dataContext.QueueCleanerConfigs
.AsNoTracking()
.FirstAsync(cancellationToken);
ContentBlockerConfig contentBlockerConfig = await dataContext.ContentBlockerConfigs
ContentBlockerConfig malwareBlockerConfig = await dataContext.ContentBlockerConfigs
.AsNoTracking()
.FirstAsync(cancellationToken);
DownloadCleanerConfig downloadCleanerConfig = await dataContext.DownloadCleanerConfigs
@@ -103,7 +103,7 @@ public class BackgroundJobManager : IHostedService
// Always register jobs, regardless of enabled status
await RegisterQueueCleanerJob(queueCleanerConfig, cancellationToken);
await RegisterContentBlockerJob(contentBlockerConfig, cancellationToken);
await RegisterMalwareBlockerJob(malwareBlockerConfig, cancellationToken);
await RegisterDownloadCleanerJob(downloadCleanerConfig, cancellationToken);
}
@@ -127,7 +127,7 @@ public class BackgroundJobManager : IHostedService
/// <summary>
/// Registers the QueueCleaner job and optionally adds triggers based on configuration.
/// </summary>
public async Task RegisterContentBlockerJob(
public async Task RegisterMalwareBlockerJob(
ContentBlockerConfig config,
CancellationToken cancellationToken = default)
{

View File

@@ -3,22 +3,22 @@ using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.Arr;
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Infrastructure.Features.DownloadClient;
using Cleanuparr.Infrastructure.Features.Jobs;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Helpers;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using MassTransit;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using LogContext = Serilog.Context.LogContext;
namespace Cleanuparr.Application.Features.ContentBlocker;
namespace Cleanuparr.Application.Features.MalwareBlocker;
public sealed class MalwareBlocker : GenericHandler
{

View File

@@ -1,8 +1,8 @@
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Microsoft.Extensions.Logging;
using NSubstitute;
namespace Cleanuparr.Infrastructure.Tests.Verticals.ContentBlocker;
namespace Cleanuparr.Infrastructure.Tests.Verticals.MalwareBlocker;
public class FilenameEvaluatorFixture
{

View File

@@ -4,7 +4,7 @@ using Cleanuparr.Domain.Enums;
using Shouldly;
using Xunit;
namespace Cleanuparr.Infrastructure.Tests.Verticals.ContentBlocker;
namespace Cleanuparr.Infrastructure.Tests.Verticals.MalwareBlocker;
public class FilenameEvaluatorTests : IClassFixture<FilenameEvaluatorFixture>
{

View File

@@ -1,9 +1,9 @@
using Cleanuparr.Domain.Entities.Deluge.Response;
using Cleanuparr.Domain.Exceptions;
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.Files;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Http;
using Cleanuparr.Persistence.Models.Configuration;
using Infrastructure.Interceptors;

View File

@@ -4,7 +4,7 @@ using Cleanuparr.Domain.Entities.Deluge.Response;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Extensions;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Features.DownloadClient.Deluge;
@@ -35,9 +35,9 @@ public partial class DelugeService
result.IsPrivate = download.Private;
var contentBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
if (contentBlockerConfig.IgnorePrivate && download.Private)
if (malwareBlockerConfig.IgnorePrivate && download.Private)
{
// ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
@@ -81,7 +81,7 @@ public partial class DelugeService
return;
}
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(name, malwarePatterns))
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(name, malwarePatterns))
{
_logger.LogInformation("malware file found | {file} | {title}", file.Path, download.Name);
result.ShouldRemove = true;

View File

@@ -2,10 +2,10 @@ using Cleanuparr.Domain.Entities;
using Cleanuparr.Domain.Entities.Cache;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Infrastructure.Features.Files;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Helpers;
using Cleanuparr.Infrastructure.Http;
using Cleanuparr.Persistence.Models.Configuration;

View File

@@ -1,8 +1,8 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.Files;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Http;
using Cleanuparr.Persistence.Models.Configuration;
using Infrastructure.Interceptors;

View File

@@ -1,7 +1,7 @@
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.Files;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Http;
using Cleanuparr.Persistence.Models.Configuration;
using Infrastructure.Interceptors;

View File

@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Extensions;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Microsoft.Extensions.Logging;
using QBittorrent.Client;
@@ -49,9 +49,9 @@ public partial class QBitService
result.IsPrivate = isPrivate;
var contentBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
if (contentBlockerConfig.IgnorePrivate && isPrivate)
if (malwareBlockerConfig.IgnorePrivate && isPrivate)
{
// ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
@@ -86,7 +86,7 @@ public partial class QBitService
totalFiles++;
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(file.Name, malwarePatterns))
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(file.Name, malwarePatterns))
{
_logger.LogInformation("malware file found | {file} | {title}", file.Name, download.Name);
result.ShouldRemove = true;

View File

@@ -1,7 +1,7 @@
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.Files;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Http;
using Cleanuparr.Persistence.Models.Configuration;
using Infrastructure.Interceptors;

View File

@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Extensions;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Microsoft.Extensions.Logging;
using Transmission.API.RPC.Entity;
@@ -40,9 +40,9 @@ public partial class TransmissionService
bool isPrivate = download.IsPrivate ?? false;
result.IsPrivate = isPrivate;
var contentBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
if (contentBlockerConfig.IgnorePrivate && isPrivate)
if (malwareBlockerConfig.IgnorePrivate && isPrivate)
{
// ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
@@ -69,7 +69,7 @@ public partial class TransmissionService
totalFiles++;
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(download.Files[i].Name, malwarePatterns))
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(download.Files[i].Name, malwarePatterns))
{
_logger.LogInformation("malware file found | {file} | {title}", download.Files[i].Name, download.Name);
result.ShouldRemove = true;

View File

@@ -1,7 +1,7 @@
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.ContentBlocker;
using Cleanuparr.Infrastructure.Features.Files;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Features.MalwareBlocker;
using Cleanuparr.Infrastructure.Http;
using Cleanuparr.Persistence.Models.Configuration;
using Infrastructure.Interceptors;

View File

@@ -5,7 +5,7 @@ using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Extensions;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent.Extensions;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Features.DownloadClient.UTorrent;
@@ -38,9 +38,9 @@ public partial class UTorrentService
return result;
}
var contentBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
var malwareBlockerConfig = ContextProvider.Get<ContentBlockerConfig>();
if (contentBlockerConfig.IgnorePrivate && result.IsPrivate)
if (malwareBlockerConfig.IgnorePrivate && result.IsPrivate)
{
// ignore private trackers
_logger.LogDebug("skip files check | download is private | {name}", download.Name);
@@ -66,7 +66,7 @@ public partial class UTorrentService
for (int i = 0; i < files.Count; i++)
{
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(files[i].Name, malwarePatterns))
if (malwareBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(files[i].Name, malwarePatterns))
{
_logger.LogInformation("malware file found | {file} | {title}", files[i].Name, download.Name);
result.ShouldRemove = true;

View File

@@ -9,9 +9,9 @@ using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Data.Models.Arr;
using MassTransit;

View File

@@ -6,15 +6,14 @@ using System.Text.RegularExpressions;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Helpers;
using Cleanuparr.Persistence;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Features.ContentBlocker;
namespace Cleanuparr.Infrastructure.Features.MalwareBlocker;
public sealed class BlocklistProvider
{
@@ -49,81 +48,81 @@ public sealed class BlocklistProvider
await using var scope = _scopeFactory.CreateAsyncScope();
await using var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
int changedCount = 0;
var contentBlockerConfig = await dataContext.ContentBlockerConfigs
var malwareBlockerConfig = await dataContext.ContentBlockerConfigs
.AsNoTracking()
.FirstAsync();
if (!contentBlockerConfig.Enabled)
if (!malwareBlockerConfig.Enabled)
{
_logger.LogDebug("Content blocker is disabled, skipping blocklist loading");
_logger.LogDebug("Malware Blocker is disabled, skipping blocklist loading");
return;
}
// Check and update Sonarr blocklist if needed
string sonarrHash = GenerateSettingsHash(contentBlockerConfig.Sonarr);
var sonarrInterval = GetLoadInterval(contentBlockerConfig.Sonarr.BlocklistPath);
var sonarrIdentifier = $"Sonarr_{contentBlockerConfig.Sonarr.BlocklistPath}";
string sonarrHash = GenerateSettingsHash(malwareBlockerConfig.Sonarr);
var sonarrInterval = GetLoadInterval(malwareBlockerConfig.Sonarr.BlocklistPath);
var sonarrIdentifier = $"Sonarr_{malwareBlockerConfig.Sonarr.BlocklistPath}";
if (ShouldReloadBlocklist(sonarrIdentifier, sonarrInterval) || !_configHashes.TryGetValue(InstanceType.Sonarr, out string? oldSonarrHash) || sonarrHash != oldSonarrHash)
{
_logger.LogDebug("Loading Sonarr blocklist");
await LoadPatternsAndRegexesAsync(contentBlockerConfig.Sonarr, InstanceType.Sonarr);
await LoadPatternsAndRegexesAsync(malwareBlockerConfig.Sonarr, InstanceType.Sonarr);
_configHashes[InstanceType.Sonarr] = sonarrHash;
_lastLoadTimes[sonarrIdentifier] = DateTime.UtcNow;
changedCount++;
}
// Check and update Radarr blocklist if needed
string radarrHash = GenerateSettingsHash(contentBlockerConfig.Radarr);
var radarrInterval = GetLoadInterval(contentBlockerConfig.Radarr.BlocklistPath);
var radarrIdentifier = $"Radarr_{contentBlockerConfig.Radarr.BlocklistPath}";
string radarrHash = GenerateSettingsHash(malwareBlockerConfig.Radarr);
var radarrInterval = GetLoadInterval(malwareBlockerConfig.Radarr.BlocklistPath);
var radarrIdentifier = $"Radarr_{malwareBlockerConfig.Radarr.BlocklistPath}";
if (ShouldReloadBlocklist(radarrIdentifier, radarrInterval) || !_configHashes.TryGetValue(InstanceType.Radarr, out string? oldRadarrHash) || radarrHash != oldRadarrHash)
{
_logger.LogDebug("Loading Radarr blocklist");
await LoadPatternsAndRegexesAsync(contentBlockerConfig.Radarr, InstanceType.Radarr);
await LoadPatternsAndRegexesAsync(malwareBlockerConfig.Radarr, InstanceType.Radarr);
_configHashes[InstanceType.Radarr] = radarrHash;
_lastLoadTimes[radarrIdentifier] = DateTime.UtcNow;
changedCount++;
}
// Check and update Lidarr blocklist if needed
string lidarrHash = GenerateSettingsHash(contentBlockerConfig.Lidarr);
var lidarrInterval = GetLoadInterval(contentBlockerConfig.Lidarr.BlocklistPath);
var lidarrIdentifier = $"Lidarr_{contentBlockerConfig.Lidarr.BlocklistPath}";
string lidarrHash = GenerateSettingsHash(malwareBlockerConfig.Lidarr);
var lidarrInterval = GetLoadInterval(malwareBlockerConfig.Lidarr.BlocklistPath);
var lidarrIdentifier = $"Lidarr_{malwareBlockerConfig.Lidarr.BlocklistPath}";
if (ShouldReloadBlocklist(lidarrIdentifier, lidarrInterval) || !_configHashes.TryGetValue(InstanceType.Lidarr, out string? oldLidarrHash) || lidarrHash != oldLidarrHash)
{
_logger.LogDebug("Loading Lidarr blocklist");
await LoadPatternsAndRegexesAsync(contentBlockerConfig.Lidarr, InstanceType.Lidarr);
await LoadPatternsAndRegexesAsync(malwareBlockerConfig.Lidarr, InstanceType.Lidarr);
_configHashes[InstanceType.Lidarr] = lidarrHash;
_lastLoadTimes[lidarrIdentifier] = DateTime.UtcNow;
changedCount++;
}
// Check and update Readarr blocklist if needed
string readarrHash = GenerateSettingsHash(contentBlockerConfig.Readarr);
var readarrInterval = GetLoadInterval(contentBlockerConfig.Readarr.BlocklistPath);
var readarrIdentifier = $"Readarr_{contentBlockerConfig.Readarr.BlocklistPath}";
string readarrHash = GenerateSettingsHash(malwareBlockerConfig.Readarr);
var readarrInterval = GetLoadInterval(malwareBlockerConfig.Readarr.BlocklistPath);
var readarrIdentifier = $"Readarr_{malwareBlockerConfig.Readarr.BlocklistPath}";
if (ShouldReloadBlocklist(readarrIdentifier, readarrInterval) || !_configHashes.TryGetValue(InstanceType.Readarr, out string? oldReadarrHash) || readarrHash != oldReadarrHash)
{
_logger.LogDebug("Loading Readarr blocklist");
await LoadPatternsAndRegexesAsync(contentBlockerConfig.Readarr, InstanceType.Readarr);
await LoadPatternsAndRegexesAsync(malwareBlockerConfig.Readarr, InstanceType.Readarr);
_configHashes[InstanceType.Readarr] = readarrHash;
_lastLoadTimes[readarrIdentifier] = DateTime.UtcNow;
changedCount++;
}
// Check and update Whisparr blocklist if needed
string whisparrHash = GenerateSettingsHash(contentBlockerConfig.Whisparr);
var whisparrInterval = GetLoadInterval(contentBlockerConfig.Whisparr.BlocklistPath);
var whisparrIdentifier = $"Whisparr_{contentBlockerConfig.Whisparr.BlocklistPath}";
string whisparrHash = GenerateSettingsHash(malwareBlockerConfig.Whisparr);
var whisparrInterval = GetLoadInterval(malwareBlockerConfig.Whisparr.BlocklistPath);
var whisparrIdentifier = $"Whisparr_{malwareBlockerConfig.Whisparr.BlocklistPath}";
if (ShouldReloadBlocklist(whisparrIdentifier, whisparrInterval) || !_configHashes.TryGetValue(InstanceType.Whisparr, out string? oldWhisparrHash) || whisparrHash != oldWhisparrHash)
{
_logger.LogDebug("Loading Whisparr blocklist");
await LoadPatternsAndRegexesAsync(contentBlockerConfig.Whisparr, InstanceType.Whisparr);
await LoadPatternsAndRegexesAsync(malwareBlockerConfig.Whisparr, InstanceType.Whisparr);
_configHashes[InstanceType.Whisparr] = whisparrHash;
_lastLoadTimes[whisparrIdentifier] = DateTime.UtcNow;
changedCount++;

View File

@@ -3,7 +3,7 @@ using System.Text.RegularExpressions;
using Cleanuparr.Domain.Enums;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Features.ContentBlocker;
namespace Cleanuparr.Infrastructure.Features.MalwareBlocker;
public class FilenameEvaluator : IFilenameEvaluator
{

View File

@@ -2,7 +2,7 @@
using System.Text.RegularExpressions;
using Cleanuparr.Domain.Enums;
namespace Cleanuparr.Infrastructure.Features.ContentBlocker;
namespace Cleanuparr.Infrastructure.Features.MalwareBlocker;
public interface IFilenameEvaluator
{

View File

@@ -1,125 +0,0 @@
# Enhanced Logging System
## Overview
The enhanced logging system provides a structured approach to logging with the following features:
- **Category-based logging**: Organize logs by functional areas (SYSTEM, API, JOBS, etc.)
- **Job name context**: Add job name to logs for background operations
- **Instance context**: Add instance names (Sonarr, Radarr, etc.) to relevant logs
- **Multiple output targets**: Console, files, and real-time SignalR streaming
- **Category-specific log files**: Separate log files for different categories
## Using the Logging System
### Adding Category to Logs
```csharp
// Using category constants
logger.WithCategory(LoggingCategoryConstants.System)
.LogInformation("This is a system log");
// Using direct category name
logger.WithCategory("API")
.LogInformation("This is an API log");
```
### Adding Job Name Context
```csharp
logger.WithCategory(LoggingCategoryConstants.Jobs)
.WithJob("ContentBlocker")
.LogInformation("Starting content blocking job");
```
### Adding Instance Name Context
```csharp
logger.WithCategory(LoggingCategoryConstants.Sonarr)
.WithInstance("Sonarr")
.LogInformation("Processing Sonarr data");
```
### Combined Context Example
```csharp
logger.WithCategory(LoggingCategoryConstants.Jobs)
.WithJob("QueueCleaner")
.WithInstance("Radarr")
.LogInformation("Cleaning Radarr queue");
```
## Log Storage
Logs are stored in the following locations:
- **Main log file**: `{config_path}/logs/Cleanuparr-.txt`
- **Category logs**: `{config_path}/logs/{category}-.txt` (e.g., `system-.txt`, `api-.txt`)
The log files use rolling file behavior:
- Daily rotation
- 10MB size limit for main log files
- 5MB size limit for category-specific logs
## SignalR Integration
The logging system includes real-time streaming via SignalR:
- **Hub URL**: `/hubs/logs`
- **Hub class**: `LogHub`
- **Event name**: `ReceiveLog`
### Requesting Recent Logs
When a client connects, it can request recent logs from the buffer:
```javascript
await connection.invoke("RequestRecentLogs");
```
### Log Message Format
Each log message contains:
- `timestamp`: The time the log was created
- `level`: Log level (Information, Warning, Error, etc.)
- `message`: The log message text
- `exception`: Exception details (if present)
- `category`: The log category
- `jobName`: The job name (if present)
- `instanceName`: The instance name (if present)
## How It All Works
1. The logging system is initialized during application startup
2. Logs are written to the console in real-time
3. Logs are written to files based on their category
4. Logs are buffered and sent to connected SignalR clients
5. New clients can request recent logs from the buffer
## Configuration Options
The logging configuration is loaded from the `Logging` section in appsettings.json:
```json
{
"Logging": {
"LogLevel": "Information",
"SignalR": {
"Enabled": true,
"BufferSize": 100
}
}
}
```
## Standard Categories
Use the `LoggingCategoryConstants` class to ensure consistent category naming:
- `LoggingCategoryConstants.System`: System-level logs
- `LoggingCategoryConstants.Api`: API-related logs
- `LoggingCategoryConstants.Jobs`: Job execution logs
- `LoggingCategoryConstants.Notifications`: User notification logs
- `LoggingCategoryConstants.Sonarr`: Sonarr-related logs
- `LoggingCategoryConstants.Radarr`: Radarr-related logs
- `LoggingCategoryConstants.Lidarr`: Lidarr-related logs

View File

@@ -2,9 +2,9 @@ using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence.Converters;
using Cleanuparr.Persistence.Models.Configuration;
using Cleanuparr.Persistence.Models.Configuration.Arr;
using Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
using Cleanuparr.Persistence.Models.Configuration.DownloadCleaner;
using Cleanuparr.Persistence.Models.Configuration.General;
using Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
using Cleanuparr.Persistence.Models.Configuration.Notification;
using Cleanuparr.Persistence.Models.Configuration.QueueCleaner;
using Cleanuparr.Shared.Helpers;

View File

@@ -1,7 +1,7 @@
using System.ComponentModel.DataAnnotations.Schema;
using Cleanuparr.Domain.Enums;
namespace Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
namespace Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
/// <summary>
/// Settings for a blocklist

View File

@@ -2,7 +2,7 @@
using System.ComponentModel.DataAnnotations.Schema;
using ValidationException = System.ComponentModel.DataAnnotations.ValidationException;
namespace Cleanuparr.Persistence.Models.Configuration.ContentBlocker;
namespace Cleanuparr.Persistence.Models.Configuration.MalwareBlocker;
public sealed record ContentBlockerConfig : IJobConfig
{

View File

@@ -18,8 +18,8 @@ export const routes: Routes = [
canDeactivate: [pendingChangesGuard]
},
{
path: 'content-blocker',
loadComponent: () => import('./settings/content-blocker/content-blocker-settings.component').then(m => m.ContentBlockerSettingsComponent),
path: 'malware-blocker',
loadComponent: () => import('./settings/malware-blocker/malware-blocker-settings.component').then(m => m.MalwareBlockerSettingsComponent),
canDeactivate: [pendingChangesGuard]
},
{

View File

@@ -2,7 +2,7 @@ import { HttpClient } from "@angular/common/http";
import { Injectable, inject } from "@angular/core";
import { Observable, catchError, map, throwError } from "rxjs";
import { JobSchedule, QueueCleanerConfig, ScheduleUnit } from "../../shared/models/queue-cleaner-config.model";
import { ContentBlockerConfig, JobSchedule as ContentBlockerJobSchedule, ScheduleUnit as ContentBlockerScheduleUnit } from "../../shared/models/content-blocker-config.model";
import { MalwareBlockerConfig as MalwareBlockerConfig, JobSchedule as MalwareBlockerJobSchedule, ScheduleUnit as MalwareBlockerScheduleUnit } from "../../shared/models/malware-blocker-config.model";
import { SonarrConfig } from "../../shared/models/sonarr-config.model";
import { RadarrConfig } from "../../shared/models/radarr-config.model";
import { LidarrConfig } from "../../shared/models/lidarr-config.model";
@@ -81,15 +81,15 @@ export class ConfigurationService {
/**
* Get content blocker configuration
*/
getContentBlockerConfig(): Observable<ContentBlockerConfig> {
return this.http.get<ContentBlockerConfig>(this.ApplicationPathService.buildApiUrl('/configuration/content_blocker')).pipe(
getMalwareBlockerConfig(): Observable<MalwareBlockerConfig> {
return this.http.get<MalwareBlockerConfig>(this.ApplicationPathService.buildApiUrl('/configuration/malware_blocker')).pipe(
map((response) => {
response.jobSchedule = this.tryExtractContentBlockerJobScheduleFromCron(response.cronExpression);
response.jobSchedule = this.tryExtractMalwareBlockerJobScheduleFromCron(response.cronExpression);
return response;
}),
catchError((error) => {
console.error("Error fetching content blocker config:", error);
return throwError(() => new Error("Failed to load content blocker configuration"));
console.error("Error fetching Malware Blocker config:", error);
return throwError(() => new Error("Failed to load Malware Blocker configuration"));
})
);
}
@@ -97,14 +97,14 @@ export class ConfigurationService {
/**
* Update content blocker configuration
*/
updateContentBlockerConfig(config: ContentBlockerConfig): Observable<void> {
updateMalwareBlockerConfig(config: MalwareBlockerConfig): Observable<void> {
// Generate cron expression if using basic scheduling
if (!config.useAdvancedScheduling && config.jobSchedule) {
config.cronExpression = this.convertContentBlockerJobScheduleToCron(config.jobSchedule);
config.cronExpression = this.convertMalwareBlockerJobScheduleToCron(config.jobSchedule);
}
return this.http.put<void>(this.ApplicationPathService.buildApiUrl('/configuration/content_blocker'), config).pipe(
return this.http.put<void>(this.ApplicationPathService.buildApiUrl('/configuration/malware_blocker'), config).pipe(
catchError((error) => {
console.error("Error updating content blocker config:", error);
console.error("Error updating Malware Blocker config:", error);
const errorMessage = ErrorHandlerUtil.extractErrorMessage(error);
return throwError(() => new Error(errorMessage));
})
@@ -188,10 +188,10 @@ export class ConfigurationService {
}
/**
* Try to extract a ContentBlockerJobSchedule from a cron expression
* Try to extract a MalwareBlockerJobSchedule from a cron expression
* Only handles the simple cases we're generating
*/
private tryExtractContentBlockerJobScheduleFromCron(cronExpression: string): ContentBlockerJobSchedule | undefined {
private tryExtractMalwareBlockerJobScheduleFromCron(cronExpression: string): MalwareBlockerJobSchedule | undefined {
// Patterns we support:
// Seconds: */n * * ? * * * or 0/n * * ? * * * (Quartz.NET format)
// Minutes: 0 */n * ? * * * or 0 0/n * ? * * * (Quartz.NET format)
@@ -205,7 +205,7 @@ export class ConfigurationService {
if ((parts[0].startsWith("*/") || parts[0].startsWith("0/")) && parts[1] === "*") {
const seconds = parseInt(parts[0].substring(2));
if (!isNaN(seconds) && seconds > 0 && seconds < 60) {
return { every: seconds, type: ContentBlockerScheduleUnit.Seconds };
return { every: seconds, type: MalwareBlockerScheduleUnit.Seconds };
}
}
@@ -213,7 +213,7 @@ export class ConfigurationService {
if (parts[0] === "0" && (parts[1].startsWith("*/") || parts[1].startsWith("0/"))) {
const minutes = parseInt(parts[1].substring(2));
if (!isNaN(minutes) && minutes > 0 && minutes < 60) {
return { every: minutes, type: ContentBlockerScheduleUnit.Minutes };
return { every: minutes, type: MalwareBlockerScheduleUnit.Minutes };
}
}
@@ -221,7 +221,7 @@ export class ConfigurationService {
if (parts[0] === "0" && parts[1] === "0" && (parts[2].startsWith("*/") || parts[2].startsWith("0/"))) {
const hours = parseInt(parts[2].substring(2));
if (!isNaN(hours) && hours > 0 && hours < 24) {
return { every: hours, type: ContentBlockerScheduleUnit.Hours };
return { every: hours, type: MalwareBlockerScheduleUnit.Hours };
}
}
} catch (e) {
@@ -232,27 +232,27 @@ export class ConfigurationService {
}
/**
* Convert a ContentBlockerJobSchedule to a cron expression
* Convert a MalwareBlockerJobSchedule to a cron expression
*/
private convertContentBlockerJobScheduleToCron(schedule: ContentBlockerJobSchedule): string {
private convertMalwareBlockerJobScheduleToCron(schedule: MalwareBlockerJobSchedule): string {
if (!schedule || schedule.every <= 0) {
return "0/5 * * * * ?"; // Default: every 5 seconds (Quartz.NET format)
}
switch (schedule.type) {
case ContentBlockerScheduleUnit.Seconds:
case MalwareBlockerScheduleUnit.Seconds:
if (schedule.every < 60) {
return `0/${schedule.every} * * ? * * *`; // Quartz.NET format
}
break;
case ContentBlockerScheduleUnit.Minutes:
case MalwareBlockerScheduleUnit.Minutes:
if (schedule.every < 60) {
return `0 0/${schedule.every} * ? * * *`; // Quartz.NET format
}
break;
case ContentBlockerScheduleUnit.Hours:
case MalwareBlockerScheduleUnit.Hours:
if (schedule.every < 24) {
return `0 0 0/${schedule.every} ? * * *`; // Quartz.NET format
}

View File

@@ -63,8 +63,8 @@ export class DocumentationService {
'unlinkedIgnoredRootDir': 'ignored-root-directory',
'unlinkedCategories': 'unlinked-categories'
},
'content-blocker': {
'enabled': 'enable-content-blocker',
'malware-blocker': {
'enabled': 'enable-malware-blocker',
'useAdvancedScheduling': 'scheduling-mode',
'cronExpression': 'cron-expression',
'jobSchedule.every': 'run-schedule',

View File

@@ -108,7 +108,7 @@ export class SidebarContentComponent implements OnInit, OnChanges, OnDestroy {
// Settings routes
{ route: '/general-settings', navigationPath: ['settings', 'general'] },
{ route: '/queue-cleaner', navigationPath: ['settings', 'queue-cleaner'] },
{ route: '/content-blocker', navigationPath: ['settings', 'content-blocker'] },
{ route: '/malware-blocker', navigationPath: ['settings', 'malware-blocker'] },
{ route: '/download-cleaner', navigationPath: ['settings', 'download-cleaner'] },
{ route: '/notifications', navigationPath: ['settings', 'notifications'] },
@@ -224,7 +224,7 @@ export class SidebarContentComponent implements OnInit, OnChanges, OnDestroy {
children: [
{ id: 'general', label: 'General', icon: 'pi pi-cog', route: '/general-settings' },
{ id: 'queue-cleaner', label: 'Queue Cleaner', icon: 'pi pi-list', route: '/queue-cleaner' },
{ id: 'content-blocker', label: 'Malware Blocker', icon: 'pi pi-shield', route: '/content-blocker' },
{ id: 'malware-blocker', label: 'Malware Blocker', icon: 'pi pi-shield', route: '/malware-blocker' },
{ id: 'download-cleaner', label: 'Download Cleaner', icon: 'pi pi-trash', route: '/download-cleaner' },
{ id: 'notifications', label: 'Notifications', icon: 'pi pi-bell', route: '/notifications' }
]

View File

@@ -1,20 +1,20 @@
import { Injectable, inject } from '@angular/core';
import { patchState, signalStore, withHooks, withMethods, withState } from '@ngrx/signals';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { ContentBlockerConfig, JobSchedule, ScheduleUnit } from '../../shared/models/content-blocker-config.model';
import { MalwareBlockerConfig as MalwareBlockerConfig, JobSchedule, ScheduleUnit } from '../../shared/models/malware-blocker-config.model';
import { ConfigurationService } from '../../core/services/configuration.service';
import { EMPTY, Observable, catchError, switchMap, tap, throwError } from 'rxjs';
import { ErrorHandlerUtil } from '../../core/utils/error-handler.util';
export interface ContentBlockerConfigState {
config: ContentBlockerConfig | null;
export interface MalwareBlockerConfigState {
config: MalwareBlockerConfig | null;
loading: boolean;
saving: boolean;
loadError: string | null; // Only for load failures that should show "Not connected"
saveError: string | null; // Only for save failures that should show toast
}
const initialState: ContentBlockerConfigState = {
const initialState: MalwareBlockerConfigState = {
config: null,
loading: false,
saving: false,
@@ -23,17 +23,17 @@ const initialState: ContentBlockerConfigState = {
};
@Injectable()
export class ContentBlockerConfigStore extends signalStore(
export class MalwareBlockerConfigStore extends signalStore(
withState(initialState),
withMethods((store, configService = inject(ConfigurationService)) => ({
/**
* Load the content blocker configuration
* Load the malware blocker configuration
*/
loadConfig: rxMethod<void>(
pipe => pipe.pipe(
tap(() => patchState(store, { loading: true, loadError: null, saveError: null })),
switchMap(() => configService.getContentBlockerConfig().pipe(
switchMap(() => configService.getMalwareBlockerConfig().pipe(
tap({
next: (config) => patchState(store, { config, loading: false, loadError: null }),
error: (error) => {
@@ -59,10 +59,10 @@ export class ContentBlockerConfigStore extends signalStore(
/**
* Save the content blocker configuration
*/
saveConfig: rxMethod<ContentBlockerConfig>(
(config$: Observable<ContentBlockerConfig>) => config$.pipe(
saveConfig: rxMethod<MalwareBlockerConfig>(
(config$: Observable<MalwareBlockerConfig>) => config$.pipe(
tap(() => patchState(store, { saving: true, saveError: null })),
switchMap(config => configService.updateContentBlockerConfig(config).pipe(
switchMap(config => configService.updateMalwareBlockerConfig(config).pipe(
tap({
next: () => {
// Don't set config - let the form stay as-is with string enum values
@@ -94,7 +94,7 @@ export class ContentBlockerConfigStore extends signalStore(
/**
* Update config in the store without saving to the backend
*/
updateConfigLocally(config: Partial<ContentBlockerConfig>) {
updateConfigLocally(config: Partial<MalwareBlockerConfig>) {
const currentConfig = store.config();
if (currentConfig) {
patchState(store, {

View File

@@ -16,15 +16,15 @@
<div class="card-content">
<!-- Loading/Error State Component -->
<app-loading-error-state
*ngIf="contentBlockerLoading() || contentBlockerLoadError()"
[loading]="contentBlockerLoading()"
[error]="contentBlockerLoadError()"
*ngIf="malwareBlockerLoading() || malwareBlockerLoadError()"
[loading]="malwareBlockerLoading()"
[error]="malwareBlockerLoadError()"
loadingMessage="Loading settings..."
errorMessage="Could not connect to server"
></app-loading-error-state>
<!-- Form Content - only shown when not loading and no error -->
<form *ngIf="!contentBlockerLoading() && !contentBlockerLoadError()" [formGroup]="contentBlockerForm" class="p-fluid">
<form *ngIf="!malwareBlockerLoading() && !malwareBlockerLoadError()" [formGroup]="malwareBlockerForm" class="p-fluid">
<!-- Main Settings -->
<div class="field-row">
<label class="field-label">
@@ -62,7 +62,7 @@
</div>
<!-- Basic Schedule Controls - shown when useAdvancedScheduling is false -->
<div class="field-row" formGroupName="jobSchedule" *ngIf="!contentBlockerForm.get('useAdvancedScheduling')?.value">
<div class="field-row" formGroupName="jobSchedule" *ngIf="!malwareBlockerForm.get('useAdvancedScheduling')?.value">
<label class="field-label">
Run Schedule
</label>
@@ -88,12 +88,12 @@
</p-selectButton>
</div>
<small *ngIf="hasNestedError('jobSchedule', 'every', 'required')" class="p-error">This field is required</small>
<small class="form-helper-text">How often the content blocker should run</small>
<small class="form-helper-text">How often the Malware Blocker should run</small>
</div>
</div>
<!-- Advanced Schedule Controls - shown when useAdvancedScheduling is true -->
<div class="field-row" *ngIf="contentBlockerForm.get('useAdvancedScheduling')?.value">
<div class="field-row" *ngIf="malwareBlockerForm.get('useAdvancedScheduling')?.value">
<label class="field-label">
<i class="pi pi-question-circle field-info-icon"
(click)="openFieldDocs('cronExpression')"
@@ -152,7 +152,7 @@
<!-- Arr Service Settings in Accordion -->
<p-accordion [multiple]="false" [value]="activeAccordionIndices" styleClass="mt-3">
<!-- Sonarr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="0">
<p-accordion-panel [disabled]="!malwareBlockerForm.get('enabled')?.value" [value]="0">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
@@ -222,7 +222,7 @@
</p-accordion-panel>
<!-- Radarr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="1">
<p-accordion-panel [disabled]="!malwareBlockerForm.get('enabled')?.value" [value]="1">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
@@ -292,7 +292,7 @@
</p-accordion-panel>
<!-- Lidarr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="2">
<p-accordion-panel [disabled]="!malwareBlockerForm.get('enabled')?.value" [value]="2">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
@@ -362,7 +362,7 @@
</p-accordion-panel>
<!-- Readarr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="3">
<p-accordion-panel [disabled]="!malwareBlockerForm.get('enabled')?.value" [value]="3">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
@@ -432,7 +432,7 @@
</p-accordion-panel>
<!-- Whisparr Settings -->
<p-accordion-panel [disabled]="!contentBlockerForm.get('enabled')?.value" [value]="4">
<p-accordion-panel [disabled]="!malwareBlockerForm.get('enabled')?.value" [value]="4">
<p-accordion-header>
<ng-template #toggleicon let-active="active">
@if (active) {
@@ -510,9 +510,9 @@
label="Save"
icon="pi pi-save"
class="p-button-primary"
[disabled]="(!contentBlockerForm.dirty || !hasActualChanges) || contentBlockerForm.invalid || contentBlockerSaving()"
[loading]="contentBlockerSaving()"
(click)="saveContentBlockerConfig()"
[disabled]="(!malwareBlockerForm.dirty || !hasActualChanges) || malwareBlockerForm.invalid || malwareBlockerSaving()"
[loading]="malwareBlockerSaving()"
(click)="saveMalwareBlockerConfig()"
></button>
<button
pButton
@@ -520,7 +520,7 @@
label="Reset"
icon="pi pi-refresh"
class="p-button-secondary p-button-outlined ml-2"
(click)="resetContentBlockerConfig()"
(click)="resetMalwareBlockerConfig()"
></button>
</div>
</form>

View File

@@ -2,14 +2,14 @@ import { Component, EventEmitter, OnDestroy, Output, effect, inject } from "@ang
import { CommonModule } from "@angular/common";
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { ContentBlockerConfigStore } from "./content-blocker-config.store";
import { MalwareBlockerConfigStore } from "./malware-blocker-config.store";
import { CanComponentDeactivate } from "../../core/guards";
import {
ContentBlockerConfig,
MalwareBlockerConfig,
ScheduleUnit,
BlocklistType,
ScheduleOptions
} from "../../shared/models/content-blocker-config.model";
} from "../../shared/models/malware-blocker-config.model";
import { FluidModule } from 'primeng/fluid';
@@ -30,7 +30,7 @@ import { ErrorHandlerUtil } from "../../core/utils/error-handler.util";
import { DocumentationService } from "../../core/services/documentation.service";
@Component({
selector: "app-content-blocker-settings",
selector: "app-malware-blocker-settings",
standalone: true,
imports: [
CommonModule,
@@ -47,16 +47,16 @@ import { DocumentationService } from "../../core/services/documentation.service"
LoadingErrorStateComponent,
FluidModule,
],
providers: [ContentBlockerConfigStore],
templateUrl: "./content-blocker-settings.component.html",
styleUrls: ["./content-blocker-settings.component.scss"],
providers: [MalwareBlockerConfigStore],
templateUrl: "./malware-blocker-settings.component.html",
styleUrls: ["./malware-blocker-settings.component.scss"],
})
export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentDeactivate {
export class MalwareBlockerSettingsComponent implements OnDestroy, CanComponentDeactivate {
@Output() saved = new EventEmitter<void>();
@Output() error = new EventEmitter<string>();
// Content Blocker Configuration Form
contentBlockerForm: FormGroup;
malwareBlockerForm: FormGroup;
// Original form values for tracking changes
private originalFormValues: any;
@@ -88,15 +88,15 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
private formBuilder = inject(FormBuilder);
// Using the notification service for all toast messages
private notificationService = inject(NotificationService);
private contentBlockerStore = inject(ContentBlockerConfigStore);
private malwareBlockerStore = inject(MalwareBlockerConfigStore);
private documentationService = inject(DocumentationService);
// Signals from the store
readonly contentBlockerConfig = this.contentBlockerStore.config;
readonly contentBlockerLoading = this.contentBlockerStore.loading;
readonly contentBlockerSaving = this.contentBlockerStore.saving;
readonly contentBlockerLoadError = this.contentBlockerStore.loadError; // Only for "Not connected" state
readonly contentBlockerSaveError = this.contentBlockerStore.saveError; // Only for toast notifications
readonly malwareBlockerConfig = this.malwareBlockerStore.config;
readonly malwareBlockerLoading = this.malwareBlockerStore.loading;
readonly malwareBlockerSaving = this.malwareBlockerStore.saving;
readonly malwareBlockerLoadError = this.malwareBlockerStore.loadError; // Only for "Not connected" state
readonly malwareBlockerSaveError = this.malwareBlockerStore.saveError; // Only for toast notifications
// Track active accordion tabs
activeAccordionIndices: number[] = [];
@@ -108,7 +108,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
* Check if component can be deactivated (navigation guard)
*/
canDeactivate(): boolean {
return !this.contentBlockerForm.dirty;
return !this.malwareBlockerForm.dirty;
}
/**
@@ -116,12 +116,12 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
* @param fieldName Field name to open documentation for
*/
openFieldDocs(fieldName: string): void {
this.documentationService.openFieldDocumentation('content-blocker', fieldName);
this.documentationService.openFieldDocumentation('malware-blocker', fieldName);
}
constructor() {
// Initialize the content blocker form with proper disabled states
this.contentBlockerForm = this.formBuilder.group({
this.malwareBlockerForm = this.formBuilder.group({
enabled: [false],
useAdvancedScheduling: [{ value: false, disabled: true }],
cronExpression: [{ value: '', disabled: true }, [Validators.required]],
@@ -164,7 +164,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
// Create an effect to update the form when the configuration changes
effect(() => {
const config = this.contentBlockerConfig();
const config = this.malwareBlockerConfig();
if (config) {
// Handle the case where ignorePrivate is true but deletePrivate is also true
// This shouldn't happen, but if it does, correct it
@@ -176,7 +176,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
}
// Reset form with the corrected config values
this.contentBlockerForm.patchValue({
this.malwareBlockerForm.patchValue({
enabled: correctedConfig.enabled,
useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false,
cronExpression: correctedConfig.cronExpression,
@@ -201,13 +201,13 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
this.storeOriginalValues();
// Mark form as pristine since we've just loaded the data
this.contentBlockerForm.markAsPristine();
this.malwareBlockerForm.markAsPristine();
}
});
// Effect to handle load errors - emit to LoadingErrorStateComponent for "Not connected" display
effect(() => {
const loadErrorMessage = this.contentBlockerLoadError();
const loadErrorMessage = this.malwareBlockerLoadError();
if (loadErrorMessage) {
// Load errors should be shown as "Not connected to server" in LoadingErrorStateComponent
this.error.emit(loadErrorMessage);
@@ -216,7 +216,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
// Effect to handle save errors - show as toast notifications for user to fix
effect(() => {
const saveErrorMessage = this.contentBlockerSaveError();
const saveErrorMessage = this.malwareBlockerSaveError();
if (saveErrorMessage) {
// Check if this looks like a validation error from the backend
// These are typically user-fixable errors that should be shown as toasts
@@ -249,7 +249,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
*/
private setupFormValueChangeListeners(): void {
// Listen for changes to the 'enabled' control
const enabledControl = this.contentBlockerForm.get('enabled');
const enabledControl = this.malwareBlockerForm.get('enabled');
if (enabledControl) {
enabledControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((enabled: boolean) => {
@@ -258,11 +258,11 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
}
// Add listener for ignorePrivate changes
const ignorePrivateControl = this.contentBlockerForm.get('ignorePrivate');
const ignorePrivateControl = this.malwareBlockerForm.get('ignorePrivate');
if (ignorePrivateControl) {
ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((ignorePrivate: boolean) => {
const deletePrivateControl = this.contentBlockerForm.get('deletePrivate');
const deletePrivateControl = this.malwareBlockerForm.get('deletePrivate');
if (ignorePrivate && deletePrivateControl) {
// If ignoring private, uncheck and disable delete private
@@ -270,7 +270,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
deletePrivateControl.disable({ onlySelf: true });
} else if (!ignorePrivate && deletePrivateControl) {
// If not ignoring private, enable delete private (if main feature is enabled)
const mainEnabled = this.contentBlockerForm.get('enabled')?.value || false;
const mainEnabled = this.malwareBlockerForm.get('enabled')?.value || false;
if (mainEnabled) {
deletePrivateControl.enable({ onlySelf: true });
}
@@ -279,14 +279,14 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
}
// Listen for changes to the 'useAdvancedScheduling' control
const advancedControl = this.contentBlockerForm.get('useAdvancedScheduling');
const advancedControl = this.malwareBlockerForm.get('useAdvancedScheduling');
if (advancedControl) {
advancedControl.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((useAdvanced: boolean) => {
const enabled = this.contentBlockerForm.get('enabled')?.value || false;
const enabled = this.malwareBlockerForm.get('enabled')?.value || false;
if (enabled) {
const cronExpressionControl = this.contentBlockerForm.get('cronExpression');
const jobScheduleGroup = this.contentBlockerForm.get('jobSchedule') as FormGroup;
const cronExpressionControl = this.malwareBlockerForm.get('cronExpression');
const jobScheduleGroup = this.malwareBlockerForm.get('jobSchedule') as FormGroup;
const everyControl = jobScheduleGroup?.get('every');
const typeControl = jobScheduleGroup?.get('type');
@@ -304,15 +304,15 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
}
// Listen for changes to the schedule type to ensure dropdown isn't empty
const scheduleTypeControl = this.contentBlockerForm.get('jobSchedule.type');
const scheduleTypeControl = this.malwareBlockerForm.get('jobSchedule.type');
if (scheduleTypeControl) {
scheduleTypeControl.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
// Ensure the selected value is valid for the new type
const everyControl = this.contentBlockerForm.get('jobSchedule.every');
const everyControl = this.malwareBlockerForm.get('jobSchedule.every');
const currentValue = everyControl?.value;
const scheduleType = this.contentBlockerForm.get('jobSchedule.type')?.value;
const scheduleType = this.malwareBlockerForm.get('jobSchedule.type')?.value;
const validValues = ScheduleOptions[scheduleType as keyof typeof ScheduleOptions];
if (currentValue && !validValues.includes(currentValue)) {
@@ -323,7 +323,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
// Listen for changes to blocklist enabled states
['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr'].forEach(arrType => {
const enabledControl = this.contentBlockerForm.get(`${arrType}.enabled`);
const enabledControl = this.malwareBlockerForm.get(`${arrType}.enabled`);
if (enabledControl) {
enabledControl.valueChanges.pipe(takeUntil(this.destroy$))
@@ -334,7 +334,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
});
// Listen to all form changes to check for actual differences from original values
this.contentBlockerForm.valueChanges.pipe(takeUntil(this.destroy$))
this.malwareBlockerForm.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.hasActualChanges = this.formValuesChanged();
});
@@ -345,7 +345,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
*/
private storeOriginalValues(): void {
// Create a deep copy of the form values to ensure proper comparison
this.originalFormValues = JSON.parse(JSON.stringify(this.contentBlockerForm.getRawValue()));
this.originalFormValues = JSON.parse(JSON.stringify(this.malwareBlockerForm.getRawValue()));
this.hasActualChanges = false;
}
@@ -353,7 +353,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
private formValuesChanged(): boolean {
if (!this.originalFormValues) return false;
const currentValues = this.contentBlockerForm.getRawValue();
const currentValues = this.malwareBlockerForm.getRawValue();
return !this.isEqual(currentValues, this.originalFormValues);
}
@@ -383,7 +383,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
/**
* Update form control disabled states based on the configuration
*/
private updateFormControlDisabledStates(config: ContentBlockerConfig): void {
private updateFormControlDisabledStates(config: MalwareBlockerConfig): void {
// Update main form controls based on the 'enabled' state
this.updateMainControlsState(config.enabled);
@@ -401,8 +401,8 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
* Update the state of blocklist dependent controls based on the 'enabled' control value
*/
private updateBlocklistDependentControls(arrType: string, enabled: boolean): void {
const pathControl = this.contentBlockerForm.get(`${arrType}.blocklistPath`);
const typeControl = this.contentBlockerForm.get(`${arrType}.blocklistType`);
const pathControl = this.malwareBlockerForm.get(`${arrType}.blocklistPath`);
const typeControl = this.malwareBlockerForm.get(`${arrType}.blocklistType`);
const options = { onlySelf: true };
if (enabled) {
@@ -423,9 +423,9 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
* Update the state of main controls based on the 'enabled' control value
*/
private updateMainControlsState(enabled: boolean): void {
const useAdvancedScheduling = this.contentBlockerForm.get('useAdvancedScheduling')?.value || false;
const cronExpressionControl = this.contentBlockerForm.get('cronExpression');
const jobScheduleGroup = this.contentBlockerForm.get('jobSchedule') as FormGroup;
const useAdvancedScheduling = this.malwareBlockerForm.get('useAdvancedScheduling')?.value || false;
const cronExpressionControl = this.malwareBlockerForm.get('cronExpression');
const jobScheduleGroup = this.malwareBlockerForm.get('jobSchedule') as FormGroup;
const everyControl = jobScheduleGroup.get('every');
const typeControl = jobScheduleGroup.get('type');
@@ -442,16 +442,16 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
}
// Enable the useAdvancedScheduling control
const useAdvancedSchedulingControl = this.contentBlockerForm.get('useAdvancedScheduling');
const useAdvancedSchedulingControl = this.malwareBlockerForm.get('useAdvancedScheduling');
useAdvancedSchedulingControl?.enable();
// Enable content blocker specific controls
this.contentBlockerForm.get("ignorePrivate")?.enable({ onlySelf: true });
this.contentBlockerForm.get("deleteKnownMalware")?.enable({ onlySelf: true });
this.malwareBlockerForm.get("ignorePrivate")?.enable({ onlySelf: true });
this.malwareBlockerForm.get("deleteKnownMalware")?.enable({ onlySelf: true });
// Only enable deletePrivate if ignorePrivate is false
const ignorePrivate = this.contentBlockerForm.get("ignorePrivate")?.value || false;
const deletePrivateControl = this.contentBlockerForm.get("deletePrivate");
const ignorePrivate = this.malwareBlockerForm.get("ignorePrivate")?.value || false;
const deletePrivateControl = this.malwareBlockerForm.get("deletePrivate");
if (!ignorePrivate && deletePrivateControl) {
deletePrivateControl.enable({ onlySelf: true });
@@ -460,18 +460,18 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
}
// Enable blocklist settings for each Arr
this.contentBlockerForm.get("sonarr.enabled")?.enable({ onlySelf: true });
this.contentBlockerForm.get("radarr.enabled")?.enable({ onlySelf: true });
this.contentBlockerForm.get("lidarr.enabled")?.enable({ onlySelf: true });
this.contentBlockerForm.get("readarr.enabled")?.enable({ onlySelf: true });
this.contentBlockerForm.get("whisparr.enabled")?.enable({ onlySelf: true });
this.malwareBlockerForm.get("sonarr.enabled")?.enable({ onlySelf: true });
this.malwareBlockerForm.get("radarr.enabled")?.enable({ onlySelf: true });
this.malwareBlockerForm.get("lidarr.enabled")?.enable({ onlySelf: true });
this.malwareBlockerForm.get("readarr.enabled")?.enable({ onlySelf: true });
this.malwareBlockerForm.get("whisparr.enabled")?.enable({ onlySelf: true });
// Update dependent controls based on current enabled states
const sonarrEnabled = this.contentBlockerForm.get("sonarr.enabled")?.value || false;
const radarrEnabled = this.contentBlockerForm.get("radarr.enabled")?.value || false;
const lidarrEnabled = this.contentBlockerForm.get("lidarr.enabled")?.value || false;
const readarrEnabled = this.contentBlockerForm.get("readarr.enabled")?.value || false;
const whisparrEnabled = this.contentBlockerForm.get("whisparr.enabled")?.value || false;
const sonarrEnabled = this.malwareBlockerForm.get("sonarr.enabled")?.value || false;
const radarrEnabled = this.malwareBlockerForm.get("radarr.enabled")?.value || false;
const lidarrEnabled = this.malwareBlockerForm.get("lidarr.enabled")?.value || false;
const readarrEnabled = this.malwareBlockerForm.get("readarr.enabled")?.value || false;
const whisparrEnabled = this.malwareBlockerForm.get("whisparr.enabled")?.value || false;
this.updateBlocklistDependentControls('sonarr', sonarrEnabled);
this.updateBlocklistDependentControls('radarr', radarrEnabled);
@@ -485,30 +485,30 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
typeControl?.disable();
// Disable the useAdvancedScheduling control
const useAdvancedSchedulingControl = this.contentBlockerForm.get('useAdvancedScheduling');
const useAdvancedSchedulingControl = this.malwareBlockerForm.get('useAdvancedScheduling');
useAdvancedSchedulingControl?.disable();
// Disable content blocker specific controls
this.contentBlockerForm.get("ignorePrivate")?.disable({ onlySelf: true });
this.contentBlockerForm.get("deletePrivate")?.disable({ onlySelf: true });
this.contentBlockerForm.get("deleteKnownMalware")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("ignorePrivate")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("deletePrivate")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("deleteKnownMalware")?.disable({ onlySelf: true });
// Disable all blocklist settings for each Arr
this.contentBlockerForm.get("sonarr.enabled")?.disable({ onlySelf: true });
this.contentBlockerForm.get("sonarr.blocklistPath")?.disable({ onlySelf: true });
this.contentBlockerForm.get("sonarr.blocklistType")?.disable({ onlySelf: true });
this.contentBlockerForm.get("radarr.enabled")?.disable({ onlySelf: true });
this.contentBlockerForm.get("radarr.blocklistPath")?.disable({ onlySelf: true });
this.contentBlockerForm.get("radarr.blocklistType")?.disable({ onlySelf: true });
this.contentBlockerForm.get("lidarr.enabled")?.disable({ onlySelf: true });
this.contentBlockerForm.get("lidarr.blocklistPath")?.disable({ onlySelf: true });
this.contentBlockerForm.get("lidarr.blocklistType")?.disable({ onlySelf: true });
this.contentBlockerForm.get("readarr.enabled")?.disable({ onlySelf: true });
this.contentBlockerForm.get("readarr.blocklistPath")?.disable({ onlySelf: true });
this.contentBlockerForm.get("readarr.blocklistType")?.disable({ onlySelf: true });
this.contentBlockerForm.get("whisparr.enabled")?.disable({ onlySelf: true });
this.contentBlockerForm.get("whisparr.blocklistPath")?.disable({ onlySelf: true });
this.contentBlockerForm.get("whisparr.blocklistType")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("sonarr.enabled")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("sonarr.blocklistPath")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("sonarr.blocklistType")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("radarr.enabled")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("radarr.blocklistPath")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("radarr.blocklistType")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("lidarr.enabled")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("lidarr.blocklistPath")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("lidarr.blocklistType")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("readarr.enabled")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("readarr.blocklistPath")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("readarr.blocklistType")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("whisparr.enabled")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("whisparr.blocklistPath")?.disable({ onlySelf: true });
this.malwareBlockerForm.get("whisparr.blocklistType")?.disable({ onlySelf: true });
// Save current active accordion state before clearing it
this.activeAccordionIndices = [];
@@ -518,22 +518,22 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
/**
* Save the content blocker configuration
*/
saveContentBlockerConfig(): void {
saveMalwareBlockerConfig(): void {
// Mark all form controls as touched to trigger validation messages
this.markFormGroupTouched(this.contentBlockerForm);
this.markFormGroupTouched(this.malwareBlockerForm);
if (this.contentBlockerForm.valid) {
if (this.malwareBlockerForm.valid) {
// Make a copy of the form values
const formValue = this.contentBlockerForm.getRawValue();
const formValue = this.malwareBlockerForm.getRawValue();
// Create the config object to be saved
const contentBlockerConfig: ContentBlockerConfig = {
const malwareBlockerConfig: MalwareBlockerConfig = {
enabled: formValue.enabled,
useAdvancedScheduling: formValue.useAdvancedScheduling,
cronExpression: formValue.useAdvancedScheduling ?
formValue.cronExpression :
// If in basic mode, generate cron expression from the schedule
this.contentBlockerStore.generateCronExpression(formValue.jobSchedule),
this.malwareBlockerStore.generateCronExpression(formValue.jobSchedule),
jobSchedule: formValue.jobSchedule,
ignorePrivate: formValue.ignorePrivate || false,
deletePrivate: formValue.deletePrivate || false,
@@ -566,22 +566,22 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
};
// Save the configuration
this.contentBlockerStore.saveConfig(contentBlockerConfig);
this.malwareBlockerStore.saveConfig(malwareBlockerConfig);
// Setup a one-time check to mark form as pristine after successful save
const checkSaveCompletion = () => {
const saving = this.contentBlockerSaving();
const saveError = this.contentBlockerSaveError();
const saving = this.malwareBlockerSaving();
const saveError = this.malwareBlockerSaveError();
if (!saving && !saveError) {
// Mark form as pristine after successful save
this.contentBlockerForm.markAsPristine();
this.malwareBlockerForm.markAsPristine();
// Update original values reference
this.storeOriginalValues();
// Emit saved event
this.saved.emit();
// Display success message
this.notificationService.showSuccess('Content blocker configuration saved successfully.');
this.notificationService.showSuccess('Malware Blocker configuration saved successfully.');
} else if (!saving && saveError) {
// If there's a save error, we can stop checking
// Toast notification is already handled by the effect above
@@ -605,8 +605,8 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
/**
* Reset the content blocker configuration form to default values
*/
resetContentBlockerConfig(): void {
this.contentBlockerForm.reset({
resetMalwareBlockerConfig(): void {
this.malwareBlockerForm.reset({
enabled: false,
useAdvancedScheduling: false,
cronExpression: "0/5 * * * * ?",
@@ -653,7 +653,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
this.updateBlocklistDependentControls('whisparr', false);
// Mark form as dirty so the save button is enabled after reset
this.contentBlockerForm.markAsDirty();
this.malwareBlockerForm.markAsDirty();
}
/**
@@ -673,7 +673,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
* Check if a form control has an error after it's been touched
*/
hasError(controlName: string, errorName: string): boolean {
const control = this.contentBlockerForm.get(controlName);
const control = this.malwareBlockerForm.get(controlName);
return control ? control.dirty && control.hasError(errorName) : false;
}
@@ -681,7 +681,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
* Get schedule value options based on the current schedule unit type
*/
getScheduleValueOptions(): {label: string, value: number}[] {
const scheduleType = this.contentBlockerForm.get('jobSchedule.type')?.value as ScheduleUnit;
const scheduleType = this.malwareBlockerForm.get('jobSchedule.type')?.value as ScheduleUnit;
if (scheduleType === ScheduleUnit.Seconds) {
return this.scheduleValueOptions[ScheduleUnit.Seconds];
} else if (scheduleType === ScheduleUnit.Minutes) {
@@ -696,7 +696,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
* Get nested form control errors
*/
hasNestedError(parentName: string, controlName: string, errorName: string): boolean {
const parentControl = this.contentBlockerForm.get(parentName);
const parentControl = this.malwareBlockerForm.get(parentName);
if (!parentControl || !(parentControl instanceof FormGroup)) {
return false;
}

View File

@@ -29,7 +29,7 @@ export interface BlocklistSettings {
blocklistType: BlocklistType;
}
export interface ContentBlockerConfig {
export interface MalwareBlockerConfig {
enabled: boolean;
cronExpression: string;
useAdvancedScheduling: boolean;

View File

@@ -68,7 +68,8 @@ Comprehensive download management and automation features for your *arr applicat
icon="🚫"
>
- Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Content Blocker**.
- Remove and block downloads blocked by qBittorrent or by Cleanuparr's **Malware Blocker**.
- Remove and block known malware based on patterns found by the community.
</ConfigSection>

View File

@@ -16,8 +16,8 @@ This is a detailed explanation of how the recurring cleanup jobs work.
<div className={styles.section}>
<ConfigSection
id="content-blocker"
title="1. Content Blocker"
id="malware-blocker"
title="1. Malware Blocker"
description="Automatically filters and removes unwanted content based on configurable blocklists"
icon="🚫"
>
@@ -47,7 +47,7 @@ This is a detailed explanation of how the recurring cleanup jobs work.
- Check each queue item if it is **stalled (download speed is 0)**, **stuck in metadata downloading**, **failed to be imported** or **slow**.
- If it is, the item receives a **strike** and will continue to accumulate strikes every time it meets any of these conditions.
- Check each queue item if it meets one of the following condition in the download client:
- **Marked as completed, but 0 bytes have been downloaded** (due to files being blocked by qBittorrent or the **content blocker**).
- **Marked as completed, but 0 bytes have been downloaded** (due to files being blocked by qBittorrent or the **Malware Blocker**).
- All associated files are marked as **unwanted/skipped/do not download**.
- If the item **DOES NOT** match the above criteria, it will be skipped.
- If the item **DOES** match the criteria or has received the **maximum number of strikes**:

View File

@@ -10,9 +10,9 @@ import {
styles
} from '@site/src/components/documentation';
# Content Blocker
# Malware Blocker
The Content Blocker automatically blocks or removes downloads from your download client based on configurable blocklists. This helps prevent unwanted content from being downloaded and manages content filtering across your *arr applications.
The Malware Blocker automatically blocks or removes downloads from your download client based on configurable blocklists. This helps prevent unwanted content from being downloaded and manages content filtering across your *arr applications.
<div className={styles.documentationPage}>
@@ -23,12 +23,12 @@ These settings need a download client to be configured.
<div className={styles.section}>
<ConfigSection
id="enable-content-blocker"
title="Enable Content Blocker"
id="enable-malware-blocker"
title="Enable Malware Blocker"
icon="🔄"
>
When enabled, the Content Blocker will run according to the configured schedule to automatically block or remove downloads based on the configured blocklists.
When enabled, the Malware Blocker will run according to the configured schedule to automatically block or remove downloads based on the configured blocklists.
</ConfigSection>
@@ -38,7 +38,7 @@ When enabled, the Content Blocker will run according to the configured schedule
icon="📅"
>
Choose how to configure the Content Blocker schedule:
Choose how to configure the Malware Blocker schedule:
- **Basic**: Simple interval-based scheduling (every X minutes/hours/seconds)
- **Advanced**: Full cron expression control for complex schedules
@@ -50,7 +50,7 @@ Choose how to configure the Content Blocker schedule:
icon="⏲️"
>
Enter a valid Quartz.NET cron expression to control when the Content Blocker runs.
Enter a valid Quartz.NET cron expression to control when the Malware Blocker runs.
**Common Cron Examples:**
- `0 0/5 * ? * * *` - Every 5 minutes
@@ -121,7 +121,7 @@ This feature permanently deletes downloads that match malware patterns. While th
icon="✅"
>
When enabled, the Content Blocker will use the configured blocklist to filter content.
When enabled, the Malware Blocker will use the configured blocklist to filter content.
</ConfigSection>

View File

@@ -6,17 +6,17 @@ import {
styles
} from '@site/src/components/documentation';
# Using Cleanuparr's Content Blocker
# Using Cleanuparr's Malware Blocker
Configure Cleanuparr's Content Blocker feature to automatically filter downloads based on custom blocklists.
Configure Cleanuparr's Malware Blocker feature to automatically filter downloads based on custom blocklists.
<div className={styles.documentationPage}>
<div className={styles.section}>
<StepGuide>
<Step title="Enable Content Blocker">
Use **Cleanuparr** with `Content Blocker` enabled.
<Step title="Enable Malware Blocker">
Use **Cleanuparr** with `Malware Blocker` enabled.
</Step>
<Step title="Configure Blocklist">
@@ -27,7 +27,7 @@ Configure Cleanuparr's Content Blocker feature to automatically filter downloads
</Step>
<Step title="Automated Processing">
The **Content Blocker** will perform a cleanup process as described in the [How it works](/docs/how_it_works) section.
The **Malware Blocker** will perform a cleanup process as described in the [How it works](/docs/how_it_works) section.
</Step>
</StepGuide>

View File

@@ -112,8 +112,8 @@ function FeaturesSection() {
const features: FeatureCardProps[] = [
{
icon: "🚫",
title: "Content Blocking",
description: "Automatically block and remove malicious files using customizable blocklists and whitelists.",
title: "Malware Blocking",
description: "Automatically block and remove malicious files using customizable blocklists.",
color: "#dc3545"
},
{
@@ -143,7 +143,7 @@ function FeaturesSection() {
{
icon: "🔔",
title: "Smart Notifications",
description: "Get alerted about strikes, removals, and cleanup operations via Discord or Apprise.",
description: "Get alerted about strikes, removals, and cleanup operations via Notifiarr or Apprise.",
color: "#fd7e14"
}
];