mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2025-12-24 06:28:55 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
323bfc4d2e | ||
|
|
dca45585ca | ||
|
|
8b5918d221 | ||
|
|
9c227c1f59 | ||
|
|
2ad4499a6f |
14
.github/workflows/build-docker.yml
vendored
14
.github/workflows/build-docker.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
||||
githubHeadRef=${{ env.githubHeadRef }}
|
||||
latestDockerTag=""
|
||||
versionDockerTag=""
|
||||
majorVersionDockerTag=""
|
||||
minorVersionDockerTag=""
|
||||
version="0.0.1"
|
||||
|
||||
if [[ "$githubRef" =~ ^"refs/tags/" ]]; then
|
||||
@@ -36,6 +38,12 @@ jobs:
|
||||
latestDockerTag="latest"
|
||||
versionDockerTag=${branch#v}
|
||||
version=${branch#v}
|
||||
|
||||
# Extract major and minor versions for additional tags
|
||||
if [[ "$versionDockerTag" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+) ]]; then
|
||||
majorVersionDockerTag="${BASH_REMATCH[1]}"
|
||||
minorVersionDockerTag="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
||||
fi
|
||||
else
|
||||
# Determine if this run is for the main branch or another branch
|
||||
if [[ -z "$githubHeadRef" ]]; then
|
||||
@@ -58,6 +66,12 @@ jobs:
|
||||
if [ -n "$versionDockerTag" ]; then
|
||||
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$versionDockerTag"
|
||||
fi
|
||||
if [ -n "$minorVersionDockerTag" ]; then
|
||||
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$minorVersionDockerTag"
|
||||
fi
|
||||
if [ -n "$majorVersionDockerTag" ]; then
|
||||
githubTags="$githubTags,ghcr.io/cleanuparr/cleanuparr:$majorVersionDockerTag"
|
||||
fi
|
||||
|
||||
# set env vars
|
||||
echo "branch=$branch" >> $GITHUB_ENV
|
||||
|
||||
36
.github/workflows/cloudflare-pages.yml
vendored
Normal file
36
.github/workflows/cloudflare-pages.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Deploy to Cloudflare Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'Cloudflare/**'
|
||||
- 'blacklist'
|
||||
- 'blacklist_permissive'
|
||||
- 'whitelist'
|
||||
- 'whitelist_with_subtitles'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy to Cloudflare Pages
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Copy root static files to Cloudflare static directory
|
||||
run: |
|
||||
cp blacklist Cloudflare/static/
|
||||
cp blacklist_permissive Cloudflare/static/
|
||||
cp whitelist Cloudflare/static/
|
||||
cp whitelist_with_subtitles Cloudflare/static/
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }}
|
||||
workingDirectory: "Cloudflare"
|
||||
command: pages deploy . --project-name=cleanuparr
|
||||
3
Cloudflare/_headers
Normal file
3
Cloudflare/_headers
Normal file
@@ -0,0 +1,3 @@
|
||||
# Cache static files for 5 minutes
|
||||
/static/*
|
||||
Cache-Control: public, max-age=300, s-maxage=300
|
||||
2
Cloudflare/static/known_malware_file_name_patterns
Normal file
2
Cloudflare/static/known_malware_file_name_patterns
Normal file
@@ -0,0 +1,2 @@
|
||||
thepirateheaven.org
|
||||
RARBG.work
|
||||
@@ -66,27 +66,27 @@ public sealed class ContentBlocker : GenericHandler
|
||||
var readarrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Readarr));
|
||||
var whisparrConfig = ContextProvider.Get<ArrConfig>(nameof(InstanceType.Whisparr));
|
||||
|
||||
if (config.Sonarr.Enabled)
|
||||
if (config.Sonarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(sonarrConfig, InstanceType.Sonarr);
|
||||
}
|
||||
|
||||
if (config.Radarr.Enabled)
|
||||
if (config.Radarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(radarrConfig, InstanceType.Radarr);
|
||||
}
|
||||
|
||||
if (config.Lidarr.Enabled)
|
||||
if (config.Lidarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(lidarrConfig, InstanceType.Lidarr);
|
||||
}
|
||||
|
||||
if (config.Readarr.Enabled)
|
||||
if (config.Readarr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(readarrConfig, InstanceType.Readarr);
|
||||
}
|
||||
|
||||
if (config.Whisparr.Enabled)
|
||||
if (config.Whisparr.Enabled || config.DeleteKnownMalware)
|
||||
{
|
||||
await ProcessArrConfigAsync(whisparrConfig, InstanceType.Whisparr);
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
|
||||
IReadOnlyList<string> ignoredDownloads = ContextProvider.Get<GeneralConfig>(nameof(GeneralConfig)).IgnoredDownloads;
|
||||
|
||||
// Process each client separately
|
||||
var allDownloads = new List<object>();
|
||||
var downloadServiceToDownloadsMap = new Dictionary<IDownloadService, List<object>>();
|
||||
|
||||
foreach (var downloadService in downloadServices)
|
||||
{
|
||||
try
|
||||
@@ -71,24 +71,24 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
var clientDownloads = await downloadService.GetSeedingDownloads();
|
||||
if (clientDownloads?.Count > 0)
|
||||
{
|
||||
allDownloads.AddRange(clientDownloads);
|
||||
downloadServiceToDownloadsMap[downloadService] = clientDownloads;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get seeding downloads from download client");
|
||||
_logger.LogError(ex, "Failed to get seeding downloads from download client {clientName}", downloadService.ClientConfig.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (allDownloads.Count == 0)
|
||||
if (downloadServiceToDownloadsMap.Count == 0)
|
||||
{
|
||||
_logger.LogDebug("no seeding downloads found");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("found {count} seeding downloads", allDownloads.Count);
|
||||
var totalDownloads = downloadServiceToDownloadsMap.Values.Sum(x => x.Count);
|
||||
_logger.LogTrace("found {count} seeding downloads across {clientCount} clients", totalDownloads, downloadServiceToDownloadsMap.Count);
|
||||
|
||||
// List<object>? downloadsToChangeCategory = null;
|
||||
List<Tuple<IDownloadService, List<object>>> downloadServiceWithDownloads = [];
|
||||
|
||||
if (isUnlinkedEnabled)
|
||||
@@ -102,24 +102,23 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create category for download client");
|
||||
_logger.LogError(ex, "Failed to create category for download client {clientName}", downloadService.ClientConfig.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// Get downloads to change category
|
||||
foreach (var downloadService in downloadServices)
|
||||
foreach (var (downloadService, clientDownloads) in downloadServiceToDownloadsMap)
|
||||
{
|
||||
try
|
||||
{
|
||||
var clientDownloads = downloadService.FilterDownloadsToChangeCategoryAsync(allDownloads, config.UnlinkedCategories);
|
||||
if (clientDownloads?.Count > 0)
|
||||
var downloadsToChangeCategory = downloadService.FilterDownloadsToChangeCategoryAsync(clientDownloads, config.UnlinkedCategories);
|
||||
if (downloadsToChangeCategory?.Count > 0)
|
||||
{
|
||||
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, clientDownloads));
|
||||
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, downloadsToChangeCategory));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to filter downloads for category change");
|
||||
_logger.LogError(ex, "Failed to filter downloads for category change for download client {clientName}", downloadService.ClientConfig.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,16 +157,15 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
return;
|
||||
}
|
||||
|
||||
// Get downloads to clean
|
||||
downloadServiceWithDownloads = [];
|
||||
foreach (var downloadService in downloadServices)
|
||||
foreach (var (downloadService, clientDownloads) in downloadServiceToDownloadsMap)
|
||||
{
|
||||
try
|
||||
{
|
||||
var clientDownloads = downloadService.FilterDownloadsToBeCleanedAsync(allDownloads, config.Categories);
|
||||
if (clientDownloads?.Count > 0)
|
||||
var downloadsToClean = downloadService.FilterDownloadsToBeCleanedAsync(clientDownloads, config.Categories);
|
||||
if (downloadsToClean?.Count > 0)
|
||||
{
|
||||
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, clientDownloads));
|
||||
downloadServiceWithDownloads.Add(Tuple.Create(downloadService, downloadsToClean));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -176,9 +174,6 @@ public sealed class DownloadCleaner : GenericHandler
|
||||
}
|
||||
}
|
||||
|
||||
// release unused objects
|
||||
allDownloads = null;
|
||||
|
||||
_logger.LogInformation("found {count} potential downloads to clean", downloadServiceWithDownloads.Sum(x => x.Item2.Count));
|
||||
|
||||
// Process cleaning for each client
|
||||
|
||||
@@ -23,8 +23,11 @@ public sealed class BlocklistProvider
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly Dictionary<InstanceType, string> _configHashes = new();
|
||||
private static DateTime _lastLoadTime = DateTime.MinValue;
|
||||
private const int LoadIntervalHours = 4;
|
||||
private readonly Dictionary<string, DateTime> _lastLoadTimes = new();
|
||||
private const int DefaultLoadIntervalHours = 4;
|
||||
private const int FastLoadIntervalMinutes = 5;
|
||||
private const string MalwareListUrl = "https://cleanuparr.pages.dev/static/known_malware_file_name_patterns";
|
||||
private const string MalwareListKey = "MALWARE_PATTERNS";
|
||||
|
||||
public BlocklistProvider(
|
||||
ILogger<BlocklistProvider> logger,
|
||||
@@ -49,13 +52,6 @@ public sealed class BlocklistProvider
|
||||
var contentBlockerConfig = await dataContext.ContentBlockerConfigs
|
||||
.AsNoTracking()
|
||||
.FirstAsync();
|
||||
bool shouldReload = false;
|
||||
|
||||
if (_lastLoadTime.AddHours(LoadIntervalHours) < DateTime.UtcNow)
|
||||
{
|
||||
shouldReload = true;
|
||||
_lastLoadTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
if (!contentBlockerConfig.Enabled)
|
||||
{
|
||||
@@ -65,59 +61,77 @@ public sealed class BlocklistProvider
|
||||
|
||||
// Check and update Sonarr blocklist if needed
|
||||
string sonarrHash = GenerateSettingsHash(contentBlockerConfig.Sonarr);
|
||||
if (shouldReload || !_configHashes.TryGetValue(InstanceType.Sonarr, out string? oldSonarrHash) || sonarrHash != oldSonarrHash)
|
||||
var sonarrInterval = GetLoadInterval(contentBlockerConfig.Sonarr.BlocklistPath);
|
||||
var sonarrIdentifier = $"Sonarr_{contentBlockerConfig.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);
|
||||
_configHashes[InstanceType.Sonarr] = sonarrHash;
|
||||
_lastLoadTimes[sonarrIdentifier] = DateTime.UtcNow;
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
// Check and update Radarr blocklist if needed
|
||||
string radarrHash = GenerateSettingsHash(contentBlockerConfig.Radarr);
|
||||
if (shouldReload || !_configHashes.TryGetValue(InstanceType.Radarr, out string? oldRadarrHash) || radarrHash != oldRadarrHash)
|
||||
var radarrInterval = GetLoadInterval(contentBlockerConfig.Radarr.BlocklistPath);
|
||||
var radarrIdentifier = $"Radarr_{contentBlockerConfig.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);
|
||||
_configHashes[InstanceType.Radarr] = radarrHash;
|
||||
_lastLoadTimes[radarrIdentifier] = DateTime.UtcNow;
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
// Check and update Lidarr blocklist if needed
|
||||
string lidarrHash = GenerateSettingsHash(contentBlockerConfig.Lidarr);
|
||||
if (shouldReload || !_configHashes.TryGetValue(InstanceType.Lidarr, out string? oldLidarrHash) || lidarrHash != oldLidarrHash)
|
||||
var lidarrInterval = GetLoadInterval(contentBlockerConfig.Lidarr.BlocklistPath);
|
||||
var lidarrIdentifier = $"Lidarr_{contentBlockerConfig.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);
|
||||
_configHashes[InstanceType.Lidarr] = lidarrHash;
|
||||
_lastLoadTimes[lidarrIdentifier] = DateTime.UtcNow;
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
// Check and update Lidarr blocklist if needed
|
||||
// Check and update Readarr blocklist if needed
|
||||
string readarrHash = GenerateSettingsHash(contentBlockerConfig.Readarr);
|
||||
if (shouldReload || !_configHashes.TryGetValue(InstanceType.Readarr, out string? oldReadarrHash) || readarrHash != oldReadarrHash)
|
||||
var readarrInterval = GetLoadInterval(contentBlockerConfig.Readarr.BlocklistPath);
|
||||
var readarrIdentifier = $"Readarr_{contentBlockerConfig.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);
|
||||
_configHashes[InstanceType.Readarr] = readarrHash;
|
||||
_lastLoadTimes[readarrIdentifier] = DateTime.UtcNow;
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
// Check and update Whisparr blocklist if needed
|
||||
string whisparrHash = GenerateSettingsHash(contentBlockerConfig.Whisparr);
|
||||
if (shouldReload || !_configHashes.TryGetValue(InstanceType.Whisparr, out string? oldWhisparrHash) || whisparrHash != oldWhisparrHash)
|
||||
var whisparrInterval = GetLoadInterval(contentBlockerConfig.Whisparr.BlocklistPath);
|
||||
var whisparrIdentifier = $"Whisparr_{contentBlockerConfig.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);
|
||||
_configHashes[InstanceType.Whisparr] = whisparrHash;
|
||||
_lastLoadTimes[whisparrIdentifier] = DateTime.UtcNow;
|
||||
changedCount++;
|
||||
}
|
||||
|
||||
// Always check and update malware patterns
|
||||
await LoadMalwarePatternsAsync();
|
||||
|
||||
if (changedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("Successfully loaded {count} blocklists", changedCount);
|
||||
@@ -154,6 +168,77 @@ public sealed class BlocklistProvider
|
||||
|
||||
return regexes ?? [];
|
||||
}
|
||||
|
||||
public ConcurrentBag<string> GetMalwarePatterns()
|
||||
{
|
||||
_cache.TryGetValue(CacheKeys.KnownMalwarePatterns(), out ConcurrentBag<string>? patterns);
|
||||
|
||||
return patterns ?? [];
|
||||
}
|
||||
|
||||
private TimeSpan GetLoadInterval(string? path)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(path) && Uri.TryCreate(path, UriKind.Absolute, out var uri))
|
||||
{
|
||||
if (uri.Host.Equals("cleanuparr.pages.dev", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return TimeSpan.FromMinutes(FastLoadIntervalMinutes);
|
||||
}
|
||||
|
||||
return TimeSpan.FromHours(DefaultLoadIntervalHours);
|
||||
}
|
||||
|
||||
// If fast load interval for local files
|
||||
return TimeSpan.FromMinutes(FastLoadIntervalMinutes);
|
||||
}
|
||||
|
||||
private bool ShouldReloadBlocklist(string identifier, TimeSpan interval)
|
||||
{
|
||||
if (!_lastLoadTimes.TryGetValue(identifier, out DateTime lastLoad))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return DateTime.UtcNow - lastLoad >= interval;
|
||||
}
|
||||
|
||||
private async Task LoadMalwarePatternsAsync()
|
||||
{
|
||||
var malwareInterval = TimeSpan.FromMinutes(FastLoadIntervalMinutes);
|
||||
|
||||
if (!ShouldReloadBlocklist(MalwareListKey, malwareInterval))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Loading malware patterns");
|
||||
|
||||
string[] filePatterns = await ReadContentAsync(MalwareListUrl);
|
||||
|
||||
long startTime = Stopwatch.GetTimestamp();
|
||||
ParallelOptions options = new() { MaxDegreeOfParallelism = 5 };
|
||||
ConcurrentBag<string> patterns = [];
|
||||
|
||||
Parallel.ForEach(filePatterns, options, pattern =>
|
||||
{
|
||||
patterns.Add(pattern);
|
||||
});
|
||||
|
||||
TimeSpan elapsed = Stopwatch.GetElapsedTime(startTime);
|
||||
|
||||
_cache.Set(CacheKeys.KnownMalwarePatterns(), patterns);
|
||||
_lastLoadTimes[MalwareListKey] = DateTime.UtcNow;
|
||||
|
||||
_logger.LogDebug("loaded {count} known malware patterns", patterns.Count);
|
||||
_logger.LogDebug("malware patterns loaded in {elapsed} ms", elapsed.TotalMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load malware patterns from {url}", MalwareListUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPatternsAndRegexesAsync(BlocklistSettings blocklistSettings, InstanceType instanceType)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,16 @@ public class FilenameEvaluator : IFilenameEvaluator
|
||||
return IsValidAgainstPatterns(filename, type, patterns) && IsValidAgainstRegexes(filename, type, regexes);
|
||||
}
|
||||
|
||||
public bool IsKnownMalware(string filename, ConcurrentBag<string> malwarePatterns)
|
||||
{
|
||||
if (malwarePatterns.Count is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return malwarePatterns.Any(pattern => filename.Contains(pattern, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
private static bool IsValidAgainstPatterns(string filename, BlocklistType type, ConcurrentBag<string> patterns)
|
||||
{
|
||||
if (patterns.Count is 0)
|
||||
|
||||
@@ -7,4 +7,6 @@ namespace Cleanuparr.Infrastructure.Features.ContentBlocker;
|
||||
public interface IFilenameEvaluator
|
||||
{
|
||||
bool IsValid(string filename, BlocklistType type, ConcurrentBag<string> patterns, ConcurrentBag<Regex> regexes);
|
||||
|
||||
bool IsKnownMalware(string filename, ConcurrentBag<string> malwarePatterns);
|
||||
}
|
||||
@@ -69,6 +69,7 @@ public partial class DelugeService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
ProcessFiles(contents.Contents, (name, file) =>
|
||||
{
|
||||
@@ -80,7 +81,7 @@ public partial class DelugeService
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDefinitelyMalware(name))
|
||||
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", file.Path, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
|
||||
@@ -100,16 +100,6 @@ public abstract class DownloadService : IDownloadService
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract Task<BlockFilesResult> BlockUnwantedFilesAsync(string hash, IReadOnlyList<string> ignoredDownloads);
|
||||
|
||||
protected bool IsDefinitelyMalware(string filename)
|
||||
{
|
||||
if (filename.Contains("thepirateheaven.org", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void ResetStalledStrikesOnProgress(string hash, long downloaded)
|
||||
{
|
||||
|
||||
@@ -74,6 +74,7 @@ public partial class QBitService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
foreach (TorrentContent file in files)
|
||||
{
|
||||
@@ -85,7 +86,7 @@ public partial class QBitService
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (IsDefinitelyMalware(file.Name))
|
||||
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(file.Name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", file.Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
|
||||
@@ -57,6 +57,7 @@ public partial class TransmissionService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
for (int i = 0; i < download.Files.Length; i++)
|
||||
{
|
||||
@@ -68,7 +69,7 @@ public partial class TransmissionService
|
||||
|
||||
totalFiles++;
|
||||
|
||||
if (IsDefinitelyMalware(download.Files[i].Name))
|
||||
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(download.Files[i].Name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", download.Files[i].Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
|
||||
@@ -62,10 +62,11 @@ public partial class UTorrentService
|
||||
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
|
||||
ConcurrentBag<string> patterns = _blocklistProvider.GetPatterns(instanceType);
|
||||
ConcurrentBag<Regex> regexes = _blocklistProvider.GetRegexes(instanceType);
|
||||
ConcurrentBag<string> malwarePatterns = _blocklistProvider.GetMalwarePatterns();
|
||||
|
||||
for (int i = 0; i < files.Count; i++)
|
||||
{
|
||||
if (IsDefinitelyMalware(files[i].Name))
|
||||
if (contentBlockerConfig.DeleteKnownMalware && _filenameEvaluator.IsKnownMalware(files[i].Name, malwarePatterns))
|
||||
{
|
||||
_logger.LogInformation("malware file found | {file} | {title}", files[i].Name, download.Name);
|
||||
result.ShouldRemove = true;
|
||||
|
||||
@@ -10,6 +10,8 @@ public static class CacheKeys
|
||||
public static string BlocklistPatterns(InstanceType instanceType) => $"{instanceType.ToString()}_patterns";
|
||||
public static string BlocklistRegexes(InstanceType instanceType) => $"{instanceType.ToString()}_regexes";
|
||||
|
||||
public static string KnownMalwarePatterns() => "KNOWN_MALWARE_PATTERNS";
|
||||
|
||||
public static string StrikeItem(string hash, StrikeType strikeType) => $"item_{hash}_{strikeType.ToString()}";
|
||||
|
||||
public static string IgnoredDownloads(string name) => $"{name}_ignored";
|
||||
|
||||
645
code/backend/Cleanuparr.Persistence/Migrations/Data/20250801143446_AddKnownMalwareOption.Designer.cs
generated
Normal file
645
code/backend/Cleanuparr.Persistence/Migrations/Data/20250801143446_AddKnownMalwareOption.Designer.cs
generated
Normal file
@@ -0,0 +1,645 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20250801143446_AddKnownMalwareOption")]
|
||||
partial class AddKnownMalwareOption
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.6");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<short>("FailedImportMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_configs");
|
||||
|
||||
b.ToTable("arr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<Guid>("ArrConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_config_id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_arr_instances");
|
||||
|
||||
b.HasIndex("ArrConfigId")
|
||||
.HasDatabaseName("ix_arr_instances_arr_config_id");
|
||||
|
||||
b.ToTable("arr_instances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeleteKnownMalware")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_known_malware");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ignore_private");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Lidarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("lidarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("lidarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Radarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Radarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("radarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Readarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Readarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("readarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("readarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_path");
|
||||
|
||||
b1.Property<string>("BlocklistType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("sonarr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("sonarr_enabled");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Whisparr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.ContentBlockerConfig.Whisparr#BlocklistSettings", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("BlocklistPath")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("whisparr_blocklist_path");
|
||||
|
||||
b1.Property<int>("BlocklistType")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_blocklist_type");
|
||||
|
||||
b1.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("whisparr_enabled");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_content_blocker_configs");
|
||||
|
||||
b.ToTable("content_blocker_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("DownloadCleanerConfigId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_cleaner_config_id");
|
||||
|
||||
b.Property<double>("MaxRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_ratio");
|
||||
|
||||
b.Property<double>("MaxSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("max_seed_time");
|
||||
|
||||
b.Property<double>("MinSeedTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("min_seed_time");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_clean_categories");
|
||||
|
||||
b.HasIndex("DownloadCleanerConfigId")
|
||||
.HasDatabaseName("ix_clean_categories_download_cleaner_config_id");
|
||||
|
||||
b.ToTable("clean_categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.PrimitiveCollection<string>("UnlinkedCategories")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_categories");
|
||||
|
||||
b.Property<bool>("UnlinkedEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_enabled");
|
||||
|
||||
b.Property<string>("UnlinkedIgnoredRootDir")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_ignored_root_dir");
|
||||
|
||||
b.Property<string>("UnlinkedTargetCategory")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unlinked_target_category");
|
||||
|
||||
b.Property<bool>("UnlinkedUseTag")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unlinked_use_tag");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_cleaner_configs");
|
||||
|
||||
b.ToTable("download_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadClientConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("host");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<string>("TypeName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type_name");
|
||||
|
||||
b.Property<string>("UrlBase")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("url_base");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("username");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_clients");
|
||||
|
||||
b.ToTable("download_clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<bool>("DisplaySupportBanner")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("display_support_banner");
|
||||
|
||||
b.Property<bool>("DryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("dry_run");
|
||||
|
||||
b.Property<string>("EncryptionKey")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("encryption_key");
|
||||
|
||||
b.Property<string>("HttpCertificateValidation")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("http_certificate_validation");
|
||||
|
||||
b.Property<ushort>("HttpMaxRetries")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_max_retries");
|
||||
|
||||
b.Property<ushort>("HttpTimeout")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("http_timeout");
|
||||
|
||||
b.PrimitiveCollection<string>("IgnoredDownloads")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ignored_downloads");
|
||||
|
||||
b.Property<string>("LogLevel")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("log_level");
|
||||
|
||||
b.Property<ushort>("SearchDelay")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_delay");
|
||||
|
||||
b.Property<bool>("SearchEnabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_enabled");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_general_configs");
|
||||
|
||||
b.ToTable("general_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("FullUrl")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("full_url");
|
||||
|
||||
b.Property<string>("Key")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("key");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.Property<string>("Tags")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tags");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_apprise_configs");
|
||||
|
||||
b.ToTable("apprise_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ApiKey")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("api_key");
|
||||
|
||||
b.Property<string>("ChannelId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("channel_id");
|
||||
|
||||
b.Property<bool>("OnCategoryChanged")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_category_changed");
|
||||
|
||||
b.Property<bool>("OnDownloadCleaned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_download_cleaned");
|
||||
|
||||
b.Property<bool>("OnFailedImportStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_failed_import_strike");
|
||||
|
||||
b.Property<bool>("OnQueueItemDeleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_queue_item_deleted");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
b.Property<bool>("OnStalledStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_stalled_strike");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_notifiarr_configs");
|
||||
|
||||
b.ToTable("notifiarr_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CronExpression")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("enabled");
|
||||
|
||||
b.Property<bool>("UseAdvancedScheduling")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_advanced_scheduling");
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_delete_private");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_ignore_private");
|
||||
|
||||
b1.PrimitiveCollection<string>("IgnoredPatterns")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("failed_import_ignored_patterns");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("failed_import_max_strikes");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Slow", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Slow#SlowConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_delete_private");
|
||||
|
||||
b1.Property<string>("IgnoreAboveSize")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_ignore_above_size");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_max_strikes");
|
||||
|
||||
b1.Property<double>("MaxTime")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("slow_max_time");
|
||||
|
||||
b1.Property<string>("MinSpeed")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("slow_min_speed");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("slow_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.ComplexProperty<Dictionary<string, object>>("Stalled", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Stalled#StalledConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_delete_private");
|
||||
|
||||
b1.Property<ushort>("DownloadingMetadataMaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_downloading_metadata_max_strikes");
|
||||
|
||||
b1.Property<bool>("IgnorePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_ignore_private");
|
||||
|
||||
b1.Property<ushort>("MaxStrikes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_max_strikes");
|
||||
|
||||
b1.Property<bool>("ResetStrikesOnProgress")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("stalled_reset_strikes_on_progress");
|
||||
});
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_queue_cleaner_configs");
|
||||
|
||||
b.ToTable("queue_cleaner_configs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrInstance", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", "ArrConfig")
|
||||
.WithMany("Instances")
|
||||
.HasForeignKey("ArrConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_arr_instances_arr_configs_arr_config_id");
|
||||
|
||||
b.Navigation("ArrConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.CleanCategory", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", "DownloadCleanerConfig")
|
||||
.WithMany("Categories")
|
||||
.HasForeignKey("DownloadCleanerConfigId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_clean_categories_download_cleaner_configs_download_cleaner_config_id");
|
||||
|
||||
b.Navigation("DownloadCleanerConfig");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b =>
|
||||
{
|
||||
b.Navigation("Instances");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b =>
|
||||
{
|
||||
b.Navigation("Categories");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddKnownMalwareOption : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "delete_known_malware",
|
||||
table: "content_blocker_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "delete_known_malware",
|
||||
table: "content_blocker_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cron_expression");
|
||||
|
||||
b.Property<bool>("DeleteKnownMalware")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_known_malware");
|
||||
|
||||
b.Property<bool>("DeletePrivate")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("delete_private");
|
||||
|
||||
@@ -19,6 +19,8 @@ public sealed record ContentBlockerConfig : IJobConfig
|
||||
public bool IgnorePrivate { get; set; }
|
||||
|
||||
public bool DeletePrivate { get; set; }
|
||||
|
||||
public bool DeleteKnownMalware { get; set; }
|
||||
|
||||
public BlocklistSettings Sonarr { get; set; } = new();
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ export class DocumentationService {
|
||||
'jobSchedule.type': 'run-schedule',
|
||||
'ignorePrivate': 'ignore-private',
|
||||
'deletePrivate': 'delete-private',
|
||||
'deleteKnownMalware': 'delete-known-malware',
|
||||
'sonarr.enabled': 'enable-sonarr-blocklist',
|
||||
'sonarr.blocklistPath': 'sonarr-blocklist-path',
|
||||
'sonarr.blocklistType': 'sonarr-blocklist-type',
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
|
||||
<small class="form-helper-text">When enabled, private torrents will not be processed</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,20 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
|
||||
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label class="field-label">
|
||||
<i class="pi pi-info-circle field-info-icon"
|
||||
(click)="openFieldDocs('deleteKnownMalware')"
|
||||
title="Click for documentation"></i>
|
||||
Delete Known Malware
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="deleteKnownMalware" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, files matching known malware patterns will be deleted</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -132,6 +132,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
|
||||
ignorePrivate: [{ value: false, disabled: true }],
|
||||
deletePrivate: [{ value: false, disabled: true }],
|
||||
deleteKnownMalware: [{ value: false, disabled: true }],
|
||||
|
||||
// Blocklist settings for each Arr
|
||||
sonarr: this.formBuilder.group({
|
||||
@@ -165,26 +166,36 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
effect(() => {
|
||||
const config = this.contentBlockerConfig();
|
||||
if (config) {
|
||||
// Reset form with the config values
|
||||
// Handle the case where ignorePrivate is true but deletePrivate is also true
|
||||
// This shouldn't happen, but if it does, correct it
|
||||
const correctedConfig = { ...config };
|
||||
|
||||
// For Content Blocker
|
||||
if (correctedConfig.ignorePrivate && correctedConfig.deletePrivate) {
|
||||
correctedConfig.deletePrivate = false;
|
||||
}
|
||||
|
||||
// Reset form with the corrected config values
|
||||
this.contentBlockerForm.patchValue({
|
||||
enabled: config.enabled,
|
||||
useAdvancedScheduling: config.useAdvancedScheduling || false,
|
||||
cronExpression: config.cronExpression,
|
||||
jobSchedule: config.jobSchedule || {
|
||||
enabled: correctedConfig.enabled,
|
||||
useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false,
|
||||
cronExpression: correctedConfig.cronExpression,
|
||||
jobSchedule: correctedConfig.jobSchedule || {
|
||||
every: 5,
|
||||
type: ScheduleUnit.Seconds
|
||||
},
|
||||
ignorePrivate: config.ignorePrivate,
|
||||
deletePrivate: config.deletePrivate,
|
||||
sonarr: config.sonarr,
|
||||
radarr: config.radarr,
|
||||
lidarr: config.lidarr,
|
||||
readarr: config.readarr,
|
||||
whisparr: config.whisparr,
|
||||
ignorePrivate: correctedConfig.ignorePrivate,
|
||||
deletePrivate: correctedConfig.deletePrivate,
|
||||
deleteKnownMalware: correctedConfig.deleteKnownMalware,
|
||||
sonarr: correctedConfig.sonarr,
|
||||
radarr: correctedConfig.radarr,
|
||||
lidarr: correctedConfig.lidarr,
|
||||
readarr: correctedConfig.readarr,
|
||||
whisparr: correctedConfig.whisparr,
|
||||
});
|
||||
|
||||
// Update all form control states
|
||||
this.updateFormControlDisabledStates(config);
|
||||
this.updateFormControlDisabledStates(correctedConfig);
|
||||
|
||||
// Store original values for dirty checking
|
||||
this.storeOriginalValues();
|
||||
@@ -245,6 +256,27 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
this.updateMainControlsState(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
// Add listener for ignorePrivate changes
|
||||
const ignorePrivateControl = this.contentBlockerForm.get('ignorePrivate');
|
||||
if (ignorePrivateControl) {
|
||||
ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((ignorePrivate: boolean) => {
|
||||
const deletePrivateControl = this.contentBlockerForm.get('deletePrivate');
|
||||
|
||||
if (ignorePrivate && deletePrivateControl) {
|
||||
// If ignoring private, uncheck and disable delete private
|
||||
deletePrivateControl.setValue(false);
|
||||
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;
|
||||
if (mainEnabled) {
|
||||
deletePrivateControl.enable({ onlySelf: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for changes to the 'useAdvancedScheduling' control
|
||||
const advancedControl = this.contentBlockerForm.get('useAdvancedScheduling');
|
||||
@@ -415,7 +447,17 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
|
||||
// Enable content blocker specific controls
|
||||
this.contentBlockerForm.get("ignorePrivate")?.enable({ onlySelf: true });
|
||||
this.contentBlockerForm.get("deletePrivate")?.enable({ onlySelf: true });
|
||||
this.contentBlockerForm.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");
|
||||
|
||||
if (!ignorePrivate && deletePrivateControl) {
|
||||
deletePrivateControl.enable({ onlySelf: true });
|
||||
} else if (deletePrivateControl) {
|
||||
deletePrivateControl.disable({ onlySelf: true });
|
||||
}
|
||||
|
||||
// Enable blocklist settings for each Arr
|
||||
this.contentBlockerForm.get("sonarr.enabled")?.enable({ onlySelf: true });
|
||||
@@ -449,6 +491,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
// 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 });
|
||||
|
||||
// Disable all blocklist settings for each Arr
|
||||
this.contentBlockerForm.get("sonarr.enabled")?.disable({ onlySelf: true });
|
||||
@@ -494,6 +537,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
jobSchedule: formValue.jobSchedule,
|
||||
ignorePrivate: formValue.ignorePrivate || false,
|
||||
deletePrivate: formValue.deletePrivate || false,
|
||||
deleteKnownMalware: formValue.deleteKnownMalware || false,
|
||||
sonarr: formValue.sonarr || {
|
||||
enabled: false,
|
||||
blocklistPath: "",
|
||||
@@ -572,6 +616,7 @@ export class ContentBlockerSettingsComponent implements OnDestroy, CanComponentD
|
||||
},
|
||||
ignorePrivate: false,
|
||||
deletePrivate: false,
|
||||
deleteKnownMalware: false,
|
||||
sonarr: {
|
||||
enabled: false,
|
||||
blocklistPath: "",
|
||||
|
||||
@@ -274,6 +274,7 @@
|
||||
</div>
|
||||
<small *ngIf="hasError('unlinkedTargetCategory', 'required')" class="p-error">Target category is required</small>
|
||||
<small class="form-helper-text">Category to move unlinked downloads to</small>
|
||||
<small class="form-helper-text">You have to create a seeding rule for this category if you want to remove the downloads</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
|
||||
<small class="form-helper-text">When enabled, private torrents will not be checked for being failed imports</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
|
||||
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
|
||||
<small class="form-helper-text">When enabled, private torrents will not be checked for being stalled</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
|
||||
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
|
||||
</div>
|
||||
</div>
|
||||
</p-accordion-content>
|
||||
@@ -383,7 +383,7 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="ignorePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be skipped</small>
|
||||
<small class="form-helper-text">When enabled, private torrents will not be checked for being slow</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -396,7 +396,7 @@
|
||||
</label>
|
||||
<div class="field-input">
|
||||
<p-checkbox formControlName="deletePrivate" [binary]="true"></p-checkbox>
|
||||
<small class="form-helper-text">When enabled, private torrents will be deleted</small>
|
||||
<small class="form-helper-text">Enable this if you want to keep private torrents in the download client even if they are removed from the arrs</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -413,6 +413,7 @@
|
||||
[min]="0"
|
||||
placeholder="Enter minimum speed"
|
||||
helpText="Minimum speed threshold for slow downloads (e.g., 100KB/s)"
|
||||
type="speed"
|
||||
>
|
||||
</app-byte-size-input>
|
||||
</div>
|
||||
@@ -454,6 +455,7 @@
|
||||
[min]="0"
|
||||
placeholder="Enter size threshold"
|
||||
helpText="Downloads will be ignored if size exceeds, e.g., 25 GB"
|
||||
type="size"
|
||||
>
|
||||
</app-byte-size-input>
|
||||
</div>
|
||||
|
||||
@@ -176,25 +176,40 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
effect(() => {
|
||||
const config = this.queueCleanerConfig();
|
||||
if (config) {
|
||||
// Save original cron expression
|
||||
const cronExpression = config.cronExpression;
|
||||
// Handle the case where ignorePrivate is true but deletePrivate is also true
|
||||
// This shouldn't happen, but if it does, correct it
|
||||
const correctedConfig = { ...config };
|
||||
|
||||
// Reset form with the config values
|
||||
// For Queue Cleaner (apply to all sections)
|
||||
if (correctedConfig.failedImport?.ignorePrivate && correctedConfig.failedImport?.deletePrivate) {
|
||||
correctedConfig.failedImport.deletePrivate = false;
|
||||
}
|
||||
if (correctedConfig.stalled?.ignorePrivate && correctedConfig.stalled?.deletePrivate) {
|
||||
correctedConfig.stalled.deletePrivate = false;
|
||||
}
|
||||
if (correctedConfig.slow?.ignorePrivate && correctedConfig.slow?.deletePrivate) {
|
||||
correctedConfig.slow.deletePrivate = false;
|
||||
}
|
||||
|
||||
// Save original cron expression
|
||||
const cronExpression = correctedConfig.cronExpression;
|
||||
|
||||
// Reset form with the corrected config values
|
||||
this.queueCleanerForm.patchValue({
|
||||
enabled: config.enabled,
|
||||
useAdvancedScheduling: config.useAdvancedScheduling || false,
|
||||
cronExpression: config.cronExpression,
|
||||
jobSchedule: config.jobSchedule || {
|
||||
enabled: correctedConfig.enabled,
|
||||
useAdvancedScheduling: correctedConfig.useAdvancedScheduling || false,
|
||||
cronExpression: correctedConfig.cronExpression,
|
||||
jobSchedule: correctedConfig.jobSchedule || {
|
||||
every: 5,
|
||||
type: ScheduleUnit.Minutes
|
||||
},
|
||||
failedImport: config.failedImport,
|
||||
stalled: config.stalled,
|
||||
slow: config.slow,
|
||||
failedImport: correctedConfig.failedImport,
|
||||
stalled: correctedConfig.stalled,
|
||||
slow: correctedConfig.slow,
|
||||
});
|
||||
|
||||
// Then update all other dependent form control states
|
||||
this.updateFormControlDisabledStates(config);
|
||||
this.updateFormControlDisabledStates(correctedConfig);
|
||||
|
||||
// Store original values for dirty checking
|
||||
this.storeOriginalValues();
|
||||
@@ -255,6 +270,30 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
this.updateMainControlsState(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
// Add listeners for ignorePrivate changes in each section
|
||||
['failedImport', 'stalled', 'slow'].forEach(section => {
|
||||
const ignorePrivateControl = this.queueCleanerForm.get(`${section}.ignorePrivate`);
|
||||
|
||||
if (ignorePrivateControl) {
|
||||
ignorePrivateControl.valueChanges.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((ignorePrivate: boolean) => {
|
||||
const deletePrivateControl = this.queueCleanerForm.get(`${section}.deletePrivate`);
|
||||
|
||||
if (ignorePrivate && deletePrivateControl) {
|
||||
// If ignoring private, uncheck and disable delete private
|
||||
deletePrivateControl.setValue(false);
|
||||
deletePrivateControl.disable({ onlySelf: true });
|
||||
} else if (!ignorePrivate && deletePrivateControl) {
|
||||
// If not ignoring private, enable delete private (if parent section is enabled)
|
||||
const sectionEnabled = this.isSectionEnabled(section);
|
||||
if (sectionEnabled) {
|
||||
deletePrivateControl.enable({ onlySelf: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for changes to the 'useAdvancedScheduling' control
|
||||
const advancedControl = this.queueCleanerForm.get('useAdvancedScheduling');
|
||||
@@ -349,6 +388,17 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
this.originalFormValues = JSON.parse(JSON.stringify(this.queueCleanerForm.getRawValue()));
|
||||
this.hasActualChanges = false;
|
||||
}
|
||||
|
||||
// Helper method to check if a section is enabled
|
||||
private isSectionEnabled(section: string): boolean {
|
||||
const mainEnabled = this.queueCleanerForm.get('enabled')?.value || false;
|
||||
if (!mainEnabled) return false;
|
||||
|
||||
const maxStrikesControl = this.queueCleanerForm.get(`${section}.maxStrikes`);
|
||||
const maxStrikes = maxStrikesControl?.value || 0;
|
||||
|
||||
return maxStrikes >= 3;
|
||||
}
|
||||
|
||||
// Check if the current form values are different from the original values
|
||||
private formValuesChanged(): boolean {
|
||||
@@ -463,8 +513,17 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
|
||||
if (enable) {
|
||||
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.enable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.enable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("ignoredPatterns")?.enable(options);
|
||||
|
||||
// Only enable deletePrivate if ignorePrivate is false
|
||||
const ignorePrivate = this.queueCleanerForm.get("failedImport.ignorePrivate")?.value || false;
|
||||
const deletePrivateControl = this.queueCleanerForm.get("failedImport.deletePrivate");
|
||||
|
||||
if (!ignorePrivate && deletePrivateControl) {
|
||||
deletePrivateControl.enable(options);
|
||||
} else if (deletePrivateControl) {
|
||||
deletePrivateControl.disable(options);
|
||||
}
|
||||
} else {
|
||||
this.queueCleanerForm.get("failedImport")?.get("ignorePrivate")?.disable(options);
|
||||
this.queueCleanerForm.get("failedImport")?.get("deletePrivate")?.disable(options);
|
||||
@@ -482,7 +541,16 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
if (enable) {
|
||||
this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.enable(options);
|
||||
this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.enable(options);
|
||||
this.queueCleanerForm.get("stalled")?.get("deletePrivate")?.enable(options);
|
||||
|
||||
// Only enable deletePrivate if ignorePrivate is false
|
||||
const ignorePrivate = this.queueCleanerForm.get("stalled.ignorePrivate")?.value || false;
|
||||
const deletePrivateControl = this.queueCleanerForm.get("stalled.deletePrivate");
|
||||
|
||||
if (!ignorePrivate && deletePrivateControl) {
|
||||
deletePrivateControl.enable(options);
|
||||
} else if (deletePrivateControl) {
|
||||
deletePrivateControl.disable(options);
|
||||
}
|
||||
} else {
|
||||
this.queueCleanerForm.get("stalled")?.get("resetStrikesOnProgress")?.disable(options);
|
||||
this.queueCleanerForm.get("stalled")?.get("ignorePrivate")?.disable(options);
|
||||
@@ -500,10 +568,19 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea
|
||||
if (enable) {
|
||||
this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.enable(options);
|
||||
this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.enable(options);
|
||||
this.queueCleanerForm.get("slow")?.get("deletePrivate")?.enable(options);
|
||||
this.queueCleanerForm.get("slow")?.get("minSpeed")?.enable(options);
|
||||
this.queueCleanerForm.get("slow")?.get("maxTime")?.enable(options);
|
||||
this.queueCleanerForm.get("slow")?.get("ignoreAboveSize")?.enable(options);
|
||||
|
||||
// Only enable deletePrivate if ignorePrivate is false
|
||||
const ignorePrivate = this.queueCleanerForm.get("slow.ignorePrivate")?.value || false;
|
||||
const deletePrivateControl = this.queueCleanerForm.get("slow.deletePrivate");
|
||||
|
||||
if (!ignorePrivate && deletePrivateControl) {
|
||||
deletePrivateControl.enable(options);
|
||||
} else if (deletePrivateControl) {
|
||||
deletePrivateControl.disable(options);
|
||||
}
|
||||
} else {
|
||||
this.queueCleanerForm.get("slow")?.get("resetStrikesOnProgress")?.disable(options);
|
||||
this.queueCleanerForm.get("slow")?.get("ignorePrivate")?.disable(options);
|
||||
|
||||
@@ -4,7 +4,9 @@ import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModu
|
||||
import { InputNumberModule } from 'primeng/inputnumber';
|
||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
||||
|
||||
type ByteSizeUnit = 'KB' | 'MB' | 'GB' | 'TB';
|
||||
export type ByteSizeInputType = 'speed' | 'size';
|
||||
|
||||
type ByteSizeUnit = 'KB' | 'MB' | 'GB';
|
||||
|
||||
@Component({
|
||||
selector: 'app-byte-size-input',
|
||||
@@ -26,31 +28,57 @@ export class ByteSizeInputComponent implements ControlValueAccessor {
|
||||
@Input() disabled: boolean = false;
|
||||
@Input() placeholder: string = 'Enter size';
|
||||
@Input() helpText: string = '';
|
||||
@Input() type: ByteSizeInputType = 'size';
|
||||
|
||||
// Value in the selected unit
|
||||
value = signal<number | null>(null);
|
||||
|
||||
|
||||
// The selected unit
|
||||
unit = signal<ByteSizeUnit>('MB');
|
||||
|
||||
// Available units
|
||||
unitOptions = [
|
||||
{ label: 'KB', value: 'KB' },
|
||||
{ label: 'MB', value: 'MB' },
|
||||
{ label: 'GB', value: 'GB' },
|
||||
{ label: 'TB', value: 'TB' }
|
||||
];
|
||||
|
||||
// Available units, computed based on type
|
||||
get unitOptions() {
|
||||
switch (this.type) {
|
||||
case 'speed':
|
||||
return [
|
||||
{ label: 'KB/s', value: 'KB' },
|
||||
{ label: 'MB/s', value: 'MB' }
|
||||
];
|
||||
case 'size':
|
||||
default:
|
||||
return [
|
||||
{ label: 'MB', value: 'MB' },
|
||||
{ label: 'GB', value: 'GB' }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Get default unit based on type
|
||||
private getDefaultUnit(): ByteSizeUnit {
|
||||
switch (this.type) {
|
||||
case 'speed':
|
||||
return 'KB';
|
||||
case 'size':
|
||||
default:
|
||||
return 'MB';
|
||||
}
|
||||
}
|
||||
|
||||
// ControlValueAccessor interface methods
|
||||
private onChange: (value: string) => void = () => {};
|
||||
private onTouched: () => void = () => {};
|
||||
|
||||
ngOnInit(): void {
|
||||
this.unit.set(this.getDefaultUnit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the string value in format '100MB', '1.5GB', etc.
|
||||
*/
|
||||
writeValue(value: string): void {
|
||||
if (!value) {
|
||||
this.value.set(null);
|
||||
this.unit.set(this.getDefaultUnit());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,15 +90,24 @@ export class ByteSizeInputComponent implements ControlValueAccessor {
|
||||
if (match) {
|
||||
const numValue = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase() as ByteSizeUnit;
|
||||
|
||||
this.value.set(numValue);
|
||||
this.unit.set(unit);
|
||||
// Validate unit is allowed for this type
|
||||
const allowedUnits = this.unitOptions.map(opt => opt.value);
|
||||
if (allowedUnits.includes(unit)) {
|
||||
this.value.set(numValue);
|
||||
this.unit.set(unit);
|
||||
} else {
|
||||
// If unit not allowed, use default
|
||||
this.value.set(numValue);
|
||||
this.unit.set(this.getDefaultUnit());
|
||||
}
|
||||
} else {
|
||||
this.value.set(null);
|
||||
this.unit.set(this.getDefaultUnit());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing byte size value:', value, e);
|
||||
this.value.set(null);
|
||||
this.unit.set(this.getDefaultUnit());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,12 +128,10 @@ export class ByteSizeInputComponent implements ControlValueAccessor {
|
||||
*/
|
||||
updateValue(): void {
|
||||
this.onTouched();
|
||||
|
||||
if (this.value() === null) {
|
||||
this.onChange('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Format as "100MB", "1.5GB", etc.
|
||||
const formattedValue = `${this.value()}${this.unit()}`;
|
||||
this.onChange(formattedValue);
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface ContentBlockerConfig {
|
||||
|
||||
ignorePrivate: boolean;
|
||||
deletePrivate: boolean;
|
||||
deleteKnownMalware: boolean;
|
||||
|
||||
sonarr: BlocklistSettings;
|
||||
radarr: BlocklistSettings;
|
||||
|
||||
@@ -83,6 +83,25 @@ Setting this to true means private torrents will be permanently deleted, potenti
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
id="delete-known-malware"
|
||||
title="Delete Known Malware"
|
||||
icon="🦠"
|
||||
>
|
||||
|
||||
When enabled, downloads that match known malware patterns will be automatically deleted from the download client.
|
||||
|
||||
**Malware Detection Source:**
|
||||
- List is automatically fetched from: `https://cleanuparr.pages.dev/static/known_malware_file_name_patterns`
|
||||
- Updates automatically every **5 minutes**
|
||||
- Contains filename patterns known to be associated with malware
|
||||
|
||||
<EnhancedWarning>
|
||||
This feature permanently deletes downloads that match malware patterns. While the patterns are carefully curated, false positives are possible. Monitor logs carefully when first enabling this feature.
|
||||
</EnhancedWarning>
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
@@ -118,7 +137,12 @@ Path to the blocklist file or URL. This can be a local file path or a remote URL
|
||||
- `/config/sonarr-blocklist.txt`
|
||||
- `https://example.com/blocklist.txt`
|
||||
|
||||
The blocklists support the following types of patters:
|
||||
**Automatic Reload Intervals:**
|
||||
- **Cleanuparr Official Lists** (`cleanuparr.pages.dev`): Every **5 minutes**
|
||||
- **Other Remote URLs**: Every **4 hours**
|
||||
- **Local Files**: Every **5 minutes**
|
||||
|
||||
The blocklists support the following types of patterns:
|
||||
```
|
||||
*example // file name ends with "example"
|
||||
example* // file name starts with "example"
|
||||
@@ -127,6 +151,14 @@ example // file name is exactly the word "example"
|
||||
regex:<ANY_REGEX> // regex that needs to be marked at the start of the line with "regex:"
|
||||
```
|
||||
|
||||
:::tip
|
||||
Available blocklists that can be used with Sonarr and Radarr:
|
||||
- `http://cleanuparr.pages.dev/static/blacklist`
|
||||
- `http://cleanuparr.pages.dev/static/blacklist_permissive`
|
||||
- `http://cleanuparr.pages.dev/static/whitelist`
|
||||
- `http://cleanuparr.pages.dev/static/whitelist_with_subtitles`
|
||||
:::
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
<ConfigSection
|
||||
@@ -139,10 +171,6 @@ Controls how the blocklist is interpreted:
|
||||
- **Blacklist**: Files matching any pattern in the list will be blocked.
|
||||
- **Whitelist**: Only files matching patterns in the list will be allowed.
|
||||
|
||||
:::tip
|
||||
[This blacklist](https://raw.githubusercontent.com/Cleanuparr/Cleanuparr/refs/heads/main/blacklist), [this permissive blacklist](https://raw.githubusercontent.com/Cleanuparr/Cleanuparr/refs/heads/main/blacklist_permissive), [this whitelist](https://raw.githubusercontent.com/Cleanuparr/Cleanuparr/refs/heads/main/whitelist) and [this whitelist with subtitles](https://raw.githubusercontent.com/Cleanuparr/Cleanuparr/refs/heads/main/whitelist_with_subtitles) can be used for Sonarr and Radarr.
|
||||
:::
|
||||
|
||||
</ConfigSection>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user