From 736c146f2559fea238756ca70dce92ac797fbcb0 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Mon, 15 Sep 2025 22:02:03 +0300 Subject: [PATCH] Add ignored downloads setting per job (#301) --- .../Controllers/ConfigurationController.cs | 1 + .../Models/UpdateDownloadCleanerConfigDto.cs | 2 + .../DownloadCleaner/DownloadCleaner.cs | 3 +- .../Features/MalwareBlocker/MalwareBlocker.cs | 3 +- .../Features/QueueCleaner/QueueCleaner.cs | 3 +- ...1529_AddPerJobIgnoredDownloads.Designer.cs | 761 ++++++++++++++++++ ...0250915181529_AddPerJobIgnoredDownloads.cs | 51 ++ .../Data/DataContextModelSnapshot.cs | 15 + .../DownloadCleaner/DownloadCleanerConfig.cs | 2 + .../MalwareBlocker/ContentBlockerConfig.cs | 2 + .../QueueCleaner/QueueCleanerConfig.cs | 2 + .../core/services/documentation.service.ts | 3 + .../download-cleaner-settings.component.html | 29 + .../download-cleaner-settings.component.ts | 9 +- .../malware-blocker-settings.component.html | 30 + .../malware-blocker-settings.component.ts | 15 + .../queue-cleaner-settings.component.html | 30 + .../queue-cleaner-settings.component.ts | 11 + .../models/download-cleaner-config.model.ts | 2 + .../models/malware-blocker-config.model.ts | 3 +- .../models/queue-cleaner-config.model.ts | 23 +- .../configuration/download-cleaner/index.mdx | 28 + .../configuration/malware-blocker/index.mdx | 24 + .../configuration/queue-cleaner/index.mdx | 28 + 24 files changed, 1051 insertions(+), 29 deletions(-) create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.Designer.cs create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.cs diff --git a/code/backend/Cleanuparr.Api/Controllers/ConfigurationController.cs b/code/backend/Cleanuparr.Api/Controllers/ConfigurationController.cs index 818107bb..6b21553c 100644 --- a/code/backend/Cleanuparr.Api/Controllers/ConfigurationController.cs +++ b/code/backend/Cleanuparr.Api/Controllers/ConfigurationController.cs @@ -1118,6 +1118,7 @@ public class ConfigurationController : ControllerBase oldConfig.UnlinkedUseTag = newConfigDto.UnlinkedUseTag; oldConfig.UnlinkedIgnoredRootDir = newConfigDto.UnlinkedIgnoredRootDir; oldConfig.UnlinkedCategories = newConfigDto.UnlinkedCategories; + oldConfig.IgnoredDownloads = newConfigDto.IgnoredDownloads; // Handle Categories collection separately to avoid EF tracking issues // Clear existing categories diff --git a/code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs b/code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs index d89d56af..126297bf 100644 --- a/code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs +++ b/code/backend/Cleanuparr.Api/Models/UpdateDownloadCleanerConfigDto.cs @@ -29,6 +29,8 @@ public class UpdateDownloadCleanerConfigDto public string UnlinkedIgnoredRootDir { get; set; } = string.Empty; public List UnlinkedCategories { get; set; } = []; + + public List IgnoredDownloads { get; set; } = []; } public class CleanCategoryDto diff --git a/code/backend/Cleanuparr.Application/Features/DownloadCleaner/DownloadCleaner.cs b/code/backend/Cleanuparr.Application/Features/DownloadCleaner/DownloadCleaner.cs index b0caee62..7ec5bc47 100644 --- a/code/backend/Cleanuparr.Application/Features/DownloadCleaner/DownloadCleaner.cs +++ b/code/backend/Cleanuparr.Application/Features/DownloadCleaner/DownloadCleaner.cs @@ -59,7 +59,8 @@ public sealed class DownloadCleaner : GenericHandler return; } - IReadOnlyList ignoredDownloads = ContextProvider.Get(nameof(GeneralConfig)).IgnoredDownloads; + List ignoredDownloads = ContextProvider.Get(nameof(GeneralConfig)).IgnoredDownloads; + ignoredDownloads.AddRange(ContextProvider.Get().IgnoredDownloads); var downloadServiceToDownloadsMap = new Dictionary>(); diff --git a/code/backend/Cleanuparr.Application/Features/MalwareBlocker/MalwareBlocker.cs b/code/backend/Cleanuparr.Application/Features/MalwareBlocker/MalwareBlocker.cs index 284ae6c7..21a75085 100644 --- a/code/backend/Cleanuparr.Application/Features/MalwareBlocker/MalwareBlocker.cs +++ b/code/backend/Cleanuparr.Application/Features/MalwareBlocker/MalwareBlocker.cs @@ -94,7 +94,8 @@ public sealed class MalwareBlocker : GenericHandler protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType) { - IReadOnlyList ignoredDownloads = ContextProvider.Get().IgnoredDownloads; + List ignoredDownloads = ContextProvider.Get(nameof(GeneralConfig)).IgnoredDownloads; + ignoredDownloads.AddRange(ContextProvider.Get().IgnoredDownloads); using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString()); diff --git a/code/backend/Cleanuparr.Application/Features/QueueCleaner/QueueCleaner.cs b/code/backend/Cleanuparr.Application/Features/QueueCleaner/QueueCleaner.cs index fd73d902..c1f03ecc 100644 --- a/code/backend/Cleanuparr.Application/Features/QueueCleaner/QueueCleaner.cs +++ b/code/backend/Cleanuparr.Application/Features/QueueCleaner/QueueCleaner.cs @@ -54,7 +54,8 @@ public sealed class QueueCleaner : GenericHandler protected override async Task ProcessInstanceAsync(ArrInstance instance, InstanceType instanceType) { - IReadOnlyList ignoredDownloads = ContextProvider.Get().IgnoredDownloads; + List ignoredDownloads = ContextProvider.Get(nameof(GeneralConfig)).IgnoredDownloads; + ignoredDownloads.AddRange(ContextProvider.Get().IgnoredDownloads); using var _ = LogContext.PushProperty(LogProperties.Category, instanceType.ToString()); diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.Designer.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.Designer.cs new file mode 100644 index 00000000..066a061d --- /dev/null +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.Designer.cs @@ -0,0 +1,761 @@ +// +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("20250915181529_AddPerJobIgnoredDownloads")] + partial class AddPerJobIgnoredDownloads + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("FailedImportMaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_max_strikes"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ApiKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("api_key"); + + b.Property("ArrConfigId") + .HasColumnType("TEXT") + .HasColumnName("arr_config_id"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("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.DownloadCleaner.CleanCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DownloadCleanerConfigId") + .HasColumnType("TEXT") + .HasColumnName("download_cleaner_config_id"); + + b.Property("MaxRatio") + .HasColumnType("REAL") + .HasColumnName("max_ratio"); + + b.Property("MaxSeedTime") + .HasColumnType("REAL") + .HasColumnName("max_seed_time"); + + b.Property("MinSeedTime") + .HasColumnType("REAL") + .HasColumnName("min_seed_time"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("cron_expression"); + + b.Property("DeletePrivate") + .HasColumnType("INTEGER") + .HasColumnName("delete_private"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + + b.PrimitiveCollection("UnlinkedCategories") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("unlinked_categories"); + + b.Property("UnlinkedEnabled") + .HasColumnType("INTEGER") + .HasColumnName("unlinked_enabled"); + + b.Property("UnlinkedIgnoredRootDir") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("unlinked_ignored_root_dir"); + + b.Property("UnlinkedTargetCategory") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("unlinked_target_category"); + + b.Property("UnlinkedUseTag") + .HasColumnType("INTEGER") + .HasColumnName("unlinked_use_tag"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("Host") + .HasColumnType("TEXT") + .HasColumnName("host"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("Password") + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("type_name"); + + b.Property("UrlBase") + .HasColumnType("TEXT") + .HasColumnName("url_base"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("DisplaySupportBanner") + .HasColumnType("INTEGER") + .HasColumnName("display_support_banner"); + + b.Property("DryRun") + .HasColumnType("INTEGER") + .HasColumnName("dry_run"); + + b.Property("EncryptionKey") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("encryption_key"); + + b.Property("HttpCertificateValidation") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("http_certificate_validation"); + + b.Property("HttpMaxRetries") + .HasColumnType("INTEGER") + .HasColumnName("http_max_retries"); + + b.Property("HttpTimeout") + .HasColumnType("INTEGER") + .HasColumnName("http_timeout"); + + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + + b.Property("SearchDelay") + .HasColumnType("INTEGER") + .HasColumnName("search_delay"); + + b.Property("SearchEnabled") + .HasColumnType("INTEGER") + .HasColumnName("search_enabled"); + + b.ComplexProperty>("Log", "Cleanuparr.Persistence.Models.Configuration.General.GeneralConfig.Log#LoggingConfig", b1 => + { + b1.IsRequired(); + + b1.Property("ArchiveEnabled") + .HasColumnType("INTEGER") + .HasColumnName("log_archive_enabled"); + + b1.Property("ArchiveRetainedCount") + .HasColumnType("INTEGER") + .HasColumnName("log_archive_retained_count"); + + b1.Property("ArchiveTimeLimitHours") + .HasColumnType("INTEGER") + .HasColumnName("log_archive_time_limit_hours"); + + b1.Property("Level") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("log_level"); + + b1.Property("RetainedFileCount") + .HasColumnType("INTEGER") + .HasColumnName("log_retained_file_count"); + + b1.Property("RollingSizeMB") + .HasColumnType("INTEGER") + .HasColumnName("log_rolling_size_mb"); + + b1.Property("TimeLimitHours") + .HasColumnType("INTEGER") + .HasColumnName("log_time_limit_hours"); + }); + + b.HasKey("Id") + .HasName("pk_general_configs"); + + b.ToTable("general_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("cron_expression"); + + b.Property("DeleteKnownMalware") + .HasColumnType("INTEGER") + .HasColumnName("delete_known_malware"); + + b.Property("DeletePrivate") + .HasColumnType("INTEGER") + .HasColumnName("delete_private"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.Property("IgnorePrivate") + .HasColumnType("INTEGER") + .HasColumnName("ignore_private"); + + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + + b.Property("UseAdvancedScheduling") + .HasColumnType("INTEGER") + .HasColumnName("use_advanced_scheduling"); + + b.ComplexProperty>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Lidarr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("lidarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("lidarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("lidarr_enabled"); + }); + + b.ComplexProperty>("Radarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Radarr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("radarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("radarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("radarr_enabled"); + }); + + b.ComplexProperty>("Readarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Readarr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("readarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("readarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("readarr_enabled"); + }); + + b.ComplexProperty>("Sonarr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Sonarr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("sonarr_blocklist_path"); + + b1.Property("BlocklistType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sonarr_blocklist_type"); + + b1.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("sonarr_enabled"); + }); + + b.ComplexProperty>("Whisparr", "Cleanuparr.Persistence.Models.Configuration.MalwareBlocker.ContentBlockerConfig.Whisparr#BlocklistSettings", b1 => + { + b1.IsRequired(); + + b1.Property("BlocklistPath") + .HasColumnType("TEXT") + .HasColumnName("whisparr_blocklist_path"); + + b1.Property("BlocklistType") + .HasColumnType("INTEGER") + .HasColumnName("whisparr_blocklist_type"); + + b1.Property("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.Notification.AppriseConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("key"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.Property("Tags") + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("tags"); + + b.Property("Url") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_apprise_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_apprise_configs_notification_config_id"); + + b.ToTable("apprise_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT") + .HasColumnName("api_key"); + + b.Property("ChannelId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT") + .HasColumnName("channel_id"); + + b.Property("NotificationConfigId") + .HasColumnType("TEXT") + .HasColumnName("notification_config_id"); + + b.HasKey("Id") + .HasName("pk_notifiarr_configs"); + + b.HasIndex("NotificationConfigId") + .IsUnique() + .HasDatabaseName("ix_notifiarr_configs_notification_config_id"); + + b.ToTable("notifiarr_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER") + .HasColumnName("is_enabled"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("OnCategoryChanged") + .HasColumnType("INTEGER") + .HasColumnName("on_category_changed"); + + b.Property("OnDownloadCleaned") + .HasColumnType("INTEGER") + .HasColumnName("on_download_cleaned"); + + b.Property("OnFailedImportStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_failed_import_strike"); + + b.Property("OnQueueItemDeleted") + .HasColumnType("INTEGER") + .HasColumnName("on_queue_item_deleted"); + + b.Property("OnSlowStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_slow_strike"); + + b.Property("OnStalledStrike") + .HasColumnType("INTEGER") + .HasColumnName("on_stalled_strike"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("updated_at"); + + b.HasKey("Id") + .HasName("pk_notification_configs"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_notification_configs_name"); + + b.ToTable("notification_configs", (string)null); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("CronExpression") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("cron_expression"); + + b.Property("Enabled") + .HasColumnType("INTEGER") + .HasColumnName("enabled"); + + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + + b.Property("UseAdvancedScheduling") + .HasColumnType("INTEGER") + .HasColumnName("use_advanced_scheduling"); + + b.ComplexProperty>("FailedImport", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.FailedImport#FailedImportConfig", b1 => + { + b1.IsRequired(); + + b1.Property("DeletePrivate") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_delete_private"); + + b1.Property("IgnorePrivate") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_ignore_private"); + + b1.PrimitiveCollection("IgnoredPatterns") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("failed_import_ignored_patterns"); + + b1.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("failed_import_max_strikes"); + }); + + b.ComplexProperty>("Slow", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Slow#SlowConfig", b1 => + { + b1.IsRequired(); + + b1.Property("DeletePrivate") + .HasColumnType("INTEGER") + .HasColumnName("slow_delete_private"); + + b1.Property("IgnoreAboveSize") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("slow_ignore_above_size"); + + b1.Property("IgnorePrivate") + .HasColumnType("INTEGER") + .HasColumnName("slow_ignore_private"); + + b1.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("slow_max_strikes"); + + b1.Property("MaxTime") + .HasColumnType("REAL") + .HasColumnName("slow_max_time"); + + b1.Property("MinSpeed") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("slow_min_speed"); + + b1.Property("ResetStrikesOnProgress") + .HasColumnType("INTEGER") + .HasColumnName("slow_reset_strikes_on_progress"); + }); + + b.ComplexProperty>("Stalled", "Cleanuparr.Persistence.Models.Configuration.QueueCleaner.QueueCleanerConfig.Stalled#StalledConfig", b1 => + { + b1.IsRequired(); + + b1.Property("DeletePrivate") + .HasColumnType("INTEGER") + .HasColumnName("stalled_delete_private"); + + b1.Property("DownloadingMetadataMaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("stalled_downloading_metadata_max_strikes"); + + b1.Property("IgnorePrivate") + .HasColumnType("INTEGER") + .HasColumnName("stalled_ignore_private"); + + b1.Property("MaxStrikes") + .HasColumnType("INTEGER") + .HasColumnName("stalled_max_strikes"); + + b1.Property("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.Notification.AppriseConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("AppriseConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.AppriseConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_apprise_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", b => + { + b.HasOne("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", "NotificationConfig") + .WithOne("NotifiarrConfiguration") + .HasForeignKey("Cleanuparr.Persistence.Models.Configuration.Notification.NotifiarrConfig", "NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notifiarr_configs_notification_configs_notification_config_id"); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Arr.ArrConfig", b => + { + b.Navigation("Instances"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.Notification.NotificationConfig", b => + { + b.Navigation("AppriseConfiguration"); + + b.Navigation("NotifiarrConfiguration"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.cs new file mode 100644 index 00000000..bc397ad1 --- /dev/null +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250915181529_AddPerJobIgnoredDownloads.cs @@ -0,0 +1,51 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Cleanuparr.Persistence.Migrations.Data +{ + /// + public partial class AddPerJobIgnoredDownloads : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ignored_downloads", + table: "queue_cleaner_configs", + type: "TEXT", + nullable: false, + defaultValue: "[]"); + + migrationBuilder.AddColumn( + name: "ignored_downloads", + table: "download_cleaner_configs", + type: "TEXT", + nullable: false, + defaultValue: "[]"); + + migrationBuilder.AddColumn( + name: "ignored_downloads", + table: "content_blocker_configs", + type: "TEXT", + nullable: false, + defaultValue: "[]"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ignored_downloads", + table: "queue_cleaner_configs"); + + migrationBuilder.DropColumn( + name: "ignored_downloads", + table: "download_cleaner_configs"); + + migrationBuilder.DropColumn( + name: "ignored_downloads", + table: "content_blocker_configs"); + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs index 1a095902..d462e5dd 100644 --- a/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs @@ -162,6 +162,11 @@ namespace Cleanuparr.Persistence.Migrations.Data .HasColumnType("INTEGER") .HasColumnName("enabled"); + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + b.PrimitiveCollection("UnlinkedCategories") .IsRequired() .HasColumnType("TEXT") @@ -357,6 +362,11 @@ namespace Cleanuparr.Persistence.Migrations.Data .HasColumnType("INTEGER") .HasColumnName("ignore_private"); + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + b.Property("UseAdvancedScheduling") .HasColumnType("INTEGER") .HasColumnName("use_advanced_scheduling"); @@ -607,6 +617,11 @@ namespace Cleanuparr.Persistence.Migrations.Data .HasColumnType("INTEGER") .HasColumnName("enabled"); + b.PrimitiveCollection("IgnoredDownloads") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ignored_downloads"); + b.Property("UseAdvancedScheduling") .HasColumnType("INTEGER") .HasColumnName("use_advanced_scheduling"); diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs index 91207535..6548f423 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/DownloadCleaner/DownloadCleanerConfig.cs @@ -36,6 +36,8 @@ public sealed record DownloadCleanerConfig : IJobConfig public List UnlinkedCategories { get; set; } = []; + public List IgnoredDownloads { get; set; } = []; + public void Validate() { if (!Enabled) diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/MalwareBlocker/ContentBlockerConfig.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/MalwareBlocker/ContentBlockerConfig.cs index c667803b..e4f6d3ad 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/MalwareBlocker/ContentBlockerConfig.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/MalwareBlocker/ContentBlockerConfig.cs @@ -31,6 +31,8 @@ public sealed record ContentBlockerConfig : IJobConfig public BlocklistSettings Readarr { get; set; } = new(); public BlocklistSettings Whisparr { get; set; } = new(); + + public List IgnoredDownloads { get; set; } = []; public void Validate() { diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueCleanerConfig.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueCleanerConfig.cs index 375289a2..8ae5a781 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueCleanerConfig.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/QueueCleaner/QueueCleanerConfig.cs @@ -23,6 +23,8 @@ public sealed record QueueCleanerConfig : IJobConfig public StalledConfig Stalled { get; set; } = new(); public SlowConfig Slow { get; set; } = new(); + + public List IgnoredDownloads { get; set; } = []; public void Validate() { diff --git a/code/frontend/src/app/core/services/documentation.service.ts b/code/frontend/src/app/core/services/documentation.service.ts index baed7bc4..7434954e 100644 --- a/code/frontend/src/app/core/services/documentation.service.ts +++ b/code/frontend/src/app/core/services/documentation.service.ts @@ -16,6 +16,7 @@ export class DocumentationService { private readonly fieldMappings: FieldDocumentationMapping = { 'queue-cleaner': { 'enabled': 'enable-queue-cleaner', + 'ignoredDownloads': 'ignored-downloads', 'useAdvancedScheduling': 'scheduling-mode', 'cronExpression': 'cron-expression', 'failedImport.maxStrikes': 'failed-import-max-strikes', @@ -56,6 +57,7 @@ export class DocumentationService { }, 'download-cleaner': { 'enabled': 'enable-download-cleaner', + 'ignoredDownloads': 'ignored-downloads', 'useAdvancedScheduling': 'scheduling-mode', 'cronExpression': 'cron-expression', 'jobSchedule.every': 'run-schedule', @@ -73,6 +75,7 @@ export class DocumentationService { }, 'malware-blocker': { 'enabled': 'enable-malware-blocker', + 'ignoredDownloads': 'ignored-downloads', 'useAdvancedScheduling': 'scheduling-mode', 'cronExpression': 'cron-expression', 'jobSchedule.every': 'run-schedule', diff --git a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html index b5777214..26ef1039 100644 --- a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html +++ b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.html @@ -109,6 +109,35 @@ + +
+ +
+ + + + + + Downloads matching these patterns will be ignored by the download cleaner +
+
+ diff --git a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts index ff02d46a..19a6fef5 100644 --- a/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts +++ b/code/frontend/src/app/settings/download-cleaner/download-cleaner-settings.component.ts @@ -21,7 +21,6 @@ import { ButtonModule } from "primeng/button"; import { InputNumberModule } from "primeng/inputnumber"; import { AccordionModule } from "primeng/accordion"; import { SelectButtonModule } from "primeng/selectbutton"; -import { ChipsModule } from "primeng/chips"; import { ToastModule } from "primeng/toast"; import { NotificationService } from "../../core/services/notification.service"; import { SelectModule } from "primeng/select"; @@ -47,7 +46,6 @@ import { DocumentationService } from "../../core/services/documentation.service" InputNumberModule, AccordionModule, SelectButtonModule, - ChipsModule, ToastModule, SelectModule, AutoCompleteModule, @@ -148,6 +146,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent type: [{ value: ScheduleUnit.Minutes, disabled: true }, [Validators.required]] }), categories: this.formBuilder.array([]), + ignoredDownloads: [{ value: [], disabled: true }], deletePrivate: [{ value: false, disabled: true }], unlinkedEnabled: [{ value: false, disabled: true }], unlinkedTargetCategory: [{ value: 'cleanuparr-unlinked', disabled: true }, [Validators.required]], @@ -290,6 +289,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent useAdvancedScheduling: useAdvanced, cronExpression: config.cronExpression, deletePrivate: config.deletePrivate, + ignoredDownloads: config.ignoredDownloads || [], unlinkedEnabled: config.unlinkedEnabled, unlinkedTargetCategory: config.unlinkedTargetCategory, unlinkedUseTag: config.unlinkedUseTag, @@ -500,11 +500,13 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent const deletePrivateControl = this.downloadCleanerForm.get('deletePrivate'); const unlinkedEnabledControl = this.downloadCleanerForm.get('unlinkedEnabled'); const useAdvancedSchedulingControl = this.downloadCleanerForm.get('useAdvancedScheduling'); + const ignoredDownloadsControl = this.downloadCleanerForm.get('ignoredDownloads'); categoriesControl?.enable(); deletePrivateControl?.enable(); unlinkedEnabledControl?.enable(); useAdvancedSchedulingControl?.enable(); + ignoredDownloadsControl?.enable(); // Update unlinked controls based on unlinkedEnabled value const unlinkedEnabled = unlinkedEnabledControl?.value; @@ -520,11 +522,13 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent const deletePrivateControl = this.downloadCleanerForm.get('deletePrivate'); const unlinkedEnabledControl = this.downloadCleanerForm.get('unlinkedEnabled'); const useAdvancedSchedulingControl = this.downloadCleanerForm.get('useAdvancedScheduling'); + const ignoredDownloadsControl = this.downloadCleanerForm.get('ignoredDownloads'); categoriesControl?.disable(); deletePrivateControl?.disable(); unlinkedEnabledControl?.disable(); useAdvancedSchedulingControl?.disable(); + ignoredDownloadsControl?.disable(); // Always disable unlinked controls when main feature is disabled this.updateUnlinkedControlsState(false); @@ -560,6 +564,7 @@ export class DownloadCleanerSettingsComponent implements OnDestroy, CanComponent jobSchedule: formValues.jobSchedule, categories: formValues.categories, deletePrivate: formValues.deletePrivate, + ignoredDownloads: formValues.ignoredDownloads || [], unlinkedEnabled: formValues.unlinkedEnabled, unlinkedTargetCategory: formValues.unlinkedTargetCategory, unlinkedUseTag: formValues.unlinkedUseTag, diff --git a/code/frontend/src/app/settings/malware-blocker/malware-blocker-settings.component.html b/code/frontend/src/app/settings/malware-blocker/malware-blocker-settings.component.html index dfafaf08..a800d56a 100644 --- a/code/frontend/src/app/settings/malware-blocker/malware-blocker-settings.component.html +++ b/code/frontend/src/app/settings/malware-blocker/malware-blocker-settings.component.html @@ -109,6 +109,36 @@ + +
+ +
+ + + + + + Downloads matching these patterns will be ignored (e.g. hash, tag, category, label, tracker) +
+
+
+ +
+ +
+ + + + + + Downloads matching these patterns will be ignored (e.g. hash, tag, category, label, tracker) +
+
+ diff --git a/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts b/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts index 9c566740..28c3989e 100644 --- a/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts +++ b/code/frontend/src/app/settings/queue-cleaner/queue-cleaner-settings.component.ts @@ -139,6 +139,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea every: [{ value: 5, disabled: true }, [Validators.required, Validators.min(1)]], type: [{ value: ScheduleUnit.Minutes, disabled: true }], }), + ignoredDownloads: [{ value: [], disabled: true }], // Failed Import settings - nested group failedImport: this.formBuilder.group({ @@ -203,6 +204,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea every: 5, type: ScheduleUnit.Minutes }, + ignoredDownloads: correctedConfig.ignoredDownloads || [], failedImport: correctedConfig.failedImport, stalled: correctedConfig.stalled, slow: correctedConfig.slow, @@ -471,6 +473,10 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea const useAdvancedSchedulingControl = this.queueCleanerForm.get('useAdvancedScheduling'); useAdvancedSchedulingControl?.enable(); + // Enable ignored downloads control + const ignoredDownloadsControl = this.queueCleanerForm.get('ignoredDownloads'); + ignoredDownloadsControl?.enable(); + // Update individual config sections only if they are enabled const failedImportMaxStrikes = this.queueCleanerForm.get("failedImport.maxStrikes")?.value; const stalledMaxStrikes = this.queueCleanerForm.get("stalled.maxStrikes")?.value; @@ -489,6 +495,10 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea const useAdvancedSchedulingControl = this.queueCleanerForm.get('useAdvancedScheduling'); useAdvancedSchedulingControl?.disable(); + // Disable ignored downloads control + const ignoredDownloadsControl = this.queueCleanerForm.get('ignoredDownloads'); + ignoredDownloadsControl?.disable(); + // Save current active accordion state before clearing it // This will be empty when we collapse all accordions this.activeAccordionIndices = []; @@ -607,6 +617,7 @@ export class QueueCleanerSettingsComponent implements OnDestroy, CanComponentDea useAdvancedScheduling: formValue.useAdvancedScheduling, cronExpression: cronExpression, jobSchedule: formValue.jobSchedule, + ignoredDownloads: formValue.ignoredDownloads || [], failedImport: { maxStrikes: formValue.failedImport?.maxStrikes || 0, ignorePrivate: formValue.failedImport?.ignorePrivate || false, diff --git a/code/frontend/src/app/shared/models/download-cleaner-config.model.ts b/code/frontend/src/app/shared/models/download-cleaner-config.model.ts index 56f7ffd2..71317ea3 100644 --- a/code/frontend/src/app/shared/models/download-cleaner-config.model.ts +++ b/code/frontend/src/app/shared/models/download-cleaner-config.model.ts @@ -9,6 +9,7 @@ export interface DownloadCleanerConfig { jobSchedule: JobSchedule; categories: CleanCategory[]; deletePrivate: boolean; + ignoredDownloads: string[]; unlinkedEnabled: boolean; unlinkedTargetCategory: string; unlinkedUseTag: boolean; @@ -49,6 +50,7 @@ export const defaultDownloadCleanerConfig: DownloadCleanerConfig = { }, categories: [], deletePrivate: false, + ignoredDownloads: [], unlinkedEnabled: false, unlinkedTargetCategory: 'cleanuparr-unlinked', unlinkedUseTag: false, diff --git a/code/frontend/src/app/shared/models/malware-blocker-config.model.ts b/code/frontend/src/app/shared/models/malware-blocker-config.model.ts index b3ca9f21..7b7e64b0 100644 --- a/code/frontend/src/app/shared/models/malware-blocker-config.model.ts +++ b/code/frontend/src/app/shared/models/malware-blocker-config.model.ts @@ -34,11 +34,10 @@ export interface MalwareBlockerConfig { cronExpression: string; useAdvancedScheduling: boolean; jobSchedule?: JobSchedule; // UI-only field, not sent to API - + ignoredDownloads: string[]; ignorePrivate: boolean; deletePrivate: boolean; deleteKnownMalware: boolean; - sonarr: BlocklistSettings; radarr: BlocklistSettings; lidarr: BlocklistSettings; diff --git a/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts b/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts index 842091f7..b16de263 100644 --- a/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts +++ b/code/frontend/src/app/shared/models/queue-cleaner-config.model.ts @@ -18,7 +18,6 @@ export interface JobSchedule { type: ScheduleUnit; } -// Nested configuration interfaces export interface FailedImportConfig { maxStrikes: number; ignorePrivate: boolean; @@ -49,28 +48,8 @@ export interface QueueCleanerConfig { cronExpression: string; useAdvancedScheduling: boolean; jobSchedule?: JobSchedule; // UI-only field, not sent to API - - // Nested configurations + ignoredDownloads: string[]; failedImport: FailedImportConfig; stalled: StalledConfig; slow: SlowConfig; - - // Legacy flat properties for backward compatibility - // These will be mapped to/from the nested structure - failedImportMaxStrikes?: number; - failedImportIgnorePrivate?: boolean; - failedImportDeletePrivate?: boolean; - failedImportIgnorePatterns?: string[]; - stalledMaxStrikes?: number; - stalledResetStrikesOnProgress?: boolean; - stalledIgnorePrivate?: boolean; - stalledDeletePrivate?: boolean; - downloadingMetadataMaxStrikes?: number; - slowMaxStrikes?: number; - slowResetStrikesOnProgress?: boolean; - slowIgnorePrivate?: boolean; - slowDeletePrivate?: boolean; - slowMinSpeed?: string; - slowMaxTime?: number; - slowIgnoreAboveSize?: string; } diff --git a/docs/docs/configuration/download-cleaner/index.mdx b/docs/docs/configuration/download-cleaner/index.mdx index 7434ce75..681cf7b8 100644 --- a/docs/docs/configuration/download-cleaner/index.mdx +++ b/docs/docs/configuration/download-cleaner/index.mdx @@ -64,6 +64,34 @@ Enter a valid Quartz.NET cron expression to control when the Download Cleaner ru
+ + +Downloads matching these patterns will be ignored by Download Cleaner. Patterns can match any of these: +- torrent hash +- qBittorrent tag or category +- Deluge label +- Transmission category (last directory from the save location) +- µTorrent label +- torrent tracker domain + +**Examples:** +``` +fa800a7d7c443a2c3561d1f8f393c089036dade1 +tv-sonarr +qbit-tag +mytracker.com +``` + + + +
+ +
+

🌱 Seeding Settings diff --git a/docs/docs/configuration/malware-blocker/index.mdx b/docs/docs/configuration/malware-blocker/index.mdx index 15836e10..68fc60d7 100644 --- a/docs/docs/configuration/malware-blocker/index.mdx +++ b/docs/docs/configuration/malware-blocker/index.mdx @@ -32,6 +32,30 @@ When enabled, the Malware Blocker will run according to the configured schedule + + +Downloads matching these patterns will be ignored by Malware Blocker. Patterns can match any of these: +- torrent hash +- qBittorrent tag or category +- Deluge label +- Transmission category (last directory from the save location) +- µTorrent label +- torrent tracker domain + +**Examples:** +``` +fa800a7d7c443a2c3561d1f8f393c089036dade1 +tv-sonarr +qbit-tag +mytracker.com +``` + + + + + +Downloads matching these patterns will be ignored by Queue Cleaner. Patterns can match any of these: +- torrent hash +- qBittorrent tag or category +- Deluge label +- Transmission category (last directory from the save location) +- µTorrent label +- torrent tracker domain + +**Examples:** +``` +fa800a7d7c443a2c3561d1f8f393c089036dade1 +tv-sonarr +qbit-tag +mytracker.com +``` + + + +

+ +
+

Failed Import Settings