From d9140d7b5bdf5d91fc323e47262fcf2931598472 Mon Sep 17 00:00:00 2001 From: Flaminel Date: Fri, 4 Jul 2025 21:14:40 +0300 Subject: [PATCH] Add support for Apprise tags (#229) --- .../Notifications/Apprise/AppriseException.cs | 12 + .../Notifications/Apprise/ApprisePayload.cs | 2 + .../Notifications/Apprise/AppriseProvider.cs | 6 +- .../Notifications/Apprise/AppriseProxy.cs | 59 +- .../20250704152940_AddAppriseTags.Designer.cs | 624 ++++++++++++++++++ .../Data/20250704152940_AddAppriseTags.cs | 28 + .../Data/DataContextModelSnapshot.cs | 4 + .../Notification/AppriseConfig.cs | 2 + .../notification-settings.component.html | 20 +- .../notification-settings.component.ts | 3 + .../app/shared/models/apprise-config.model.ts | 1 + .../configuration/notifications/index.mdx | 10 + 12 files changed, 748 insertions(+), 23 deletions(-) create mode 100644 code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseException.cs create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.Designer.cs create mode 100644 code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.cs diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseException.cs b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseException.cs new file mode 100644 index 00000000..7b7fbf51 --- /dev/null +++ b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseException.cs @@ -0,0 +1,12 @@ +namespace Cleanuparr.Infrastructure.Features.Notifications.Apprise; + +public class AppriseException : Exception +{ + public AppriseException(string message) : base(message) + { + } + + public AppriseException(string message, Exception innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/ApprisePayload.cs b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/ApprisePayload.cs index d9150e5f..7dd637e1 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/ApprisePayload.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/ApprisePayload.cs @@ -13,6 +13,8 @@ public sealed record ApprisePayload public string Type { get; init; } = NotificationType.Info.ToString().ToLowerInvariant(); public string Format { get; init; } = FormatType.Text.ToString().ToLowerInvariant(); + + public string? Tags { get; init; } } public enum NotificationType diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProvider.cs b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProvider.cs index 246ecc63..df00fd94 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProvider.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProvider.cs @@ -48,7 +48,7 @@ public sealed class AppriseProvider : NotificationProvider await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), Config); } - private static ApprisePayload BuildPayload(ArrNotification notification, NotificationType notificationType) + private ApprisePayload BuildPayload(ArrNotification notification, NotificationType notificationType) { StringBuilder body = new(); body.AppendLine(notification.Description); @@ -67,12 +67,13 @@ public sealed class AppriseProvider : NotificationProvider Title = notification.Title, Body = body.ToString(), Type = notificationType.ToString().ToLowerInvariant(), + Tags = Config.Tags, }; return payload; } - private static ApprisePayload BuildPayload(Notification notification, NotificationType notificationType) + private ApprisePayload BuildPayload(Notification notification, NotificationType notificationType) { StringBuilder body = new(); body.AppendLine(notification.Description); @@ -88,6 +89,7 @@ public sealed class AppriseProvider : NotificationProvider Title = notification.Title, Body = body.ToString(), Type = notificationType.ToString().ToLowerInvariant(), + Tags = Config.Tags, }; return payload; diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProxy.cs b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProxy.cs index 7dc578d5..5bc291a3 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProxy.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/Notifications/Apprise/AppriseProxy.cs @@ -18,25 +18,50 @@ public sealed class AppriseProxy : IAppriseProxy public async Task SendNotification(ApprisePayload payload, AppriseConfig config) { - string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings + try { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }); - - UriBuilder uriBuilder = new(config.Url.ToString()); - uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/notify/{config.Key}"; - - using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri); - request.Method = HttpMethod.Post; - request.Content = new StringContent(content, Encoding.UTF8, "application/json"); + string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }); - if (!string.IsNullOrEmpty(config.Url.UserInfo)) - { - var byteArray = Encoding.ASCII.GetBytes(config.Url.UserInfo); - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); + UriBuilder uriBuilder = new(config.Url.ToString()); + uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/notify/{config.Key}"; + + using HttpRequestMessage request = new(HttpMethod.Post, uriBuilder.Uri); + request.Method = HttpMethod.Post; + request.Content = new StringContent(content, Encoding.UTF8, "application/json"); + + if (!string.IsNullOrEmpty(config.Url.UserInfo)) + { + var byteArray = Encoding.ASCII.GetBytes(config.Url.UserInfo); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); + } + + using HttpResponseMessage response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + catch (HttpRequestException exception) + { + if (exception.StatusCode is null) + { + throw new AppriseException("Unable to send notification", exception); + } + + switch ((int)exception.StatusCode) + { + case 401: + throw new AppriseException("Unable to send notification | API key is invalid"); + case 424: + throw new AppriseException("Your tags are not configured correctly", exception); + case 502: + case 503: + case 504: + throw new AppriseException("Unable to send notification | service unavailable", exception); + default: + throw new AppriseException("Unable to send notification", exception); + } } - - using HttpResponseMessage response = await _httpClient.SendAsync(request); - response.EnsureSuccessStatusCode(); } } \ No newline at end of file diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.Designer.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.Designer.cs new file mode 100644 index 00000000..528d432c --- /dev/null +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.Designer.cs @@ -0,0 +1,624 @@ +// +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("20250704152940_AddAppriseTags")] + partial class AddAppriseTags + { + /// + 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.ContentBlocker.ContentBlockerConfig", 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.Property("IgnorePrivate") + .HasColumnType("INTEGER") + .HasColumnName("ignore_private"); + + b.Property("UseAdvancedScheduling") + .HasColumnType("INTEGER") + .HasColumnName("use_advanced_scheduling"); + + b.ComplexProperty>("Lidarr", "Cleanuparr.Persistence.Models.Configuration.ContentBlocker.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.ContentBlocker.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.ContentBlocker.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.ContentBlocker.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.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("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("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("LogLevel") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("log_level"); + + b.Property("SearchDelay") + .HasColumnType("INTEGER") + .HasColumnName("search_delay"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("FullUrl") + .HasColumnType("TEXT") + .HasColumnName("full_url"); + + b.Property("Key") + .HasColumnType("TEXT") + .HasColumnName("key"); + + 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("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("id"); + + b.Property("ApiKey") + .HasColumnType("TEXT") + .HasColumnName("api_key"); + + b.Property("ChannelId") + .HasColumnType("TEXT") + .HasColumnName("channel_id"); + + 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.HasKey("Id") + .HasName("pk_notifiarr_configs"); + + b.ToTable("notifiarr_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.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.Arr.ArrConfig", b => + { + b.Navigation("Instances"); + }); + + modelBuilder.Entity("Cleanuparr.Persistence.Models.Configuration.DownloadCleaner.DownloadCleanerConfig", b => + { + b.Navigation("Categories"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.cs new file mode 100644 index 00000000..3b68820a --- /dev/null +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/20250704152940_AddAppriseTags.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Cleanuparr.Persistence.Migrations.Data +{ + /// + public partial class AddAppriseTags : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "tags", + table: "apprise_configs", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "tags", + table: "apprise_configs"); + } + } +} diff --git a/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs b/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs index 2d4e2747..bbd0ddaa 100644 --- a/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs +++ b/code/backend/Cleanuparr.Persistence/Migrations/Data/DataContextModelSnapshot.cs @@ -419,6 +419,10 @@ namespace Cleanuparr.Persistence.Migrations.Data .HasColumnType("INTEGER") .HasColumnName("on_stalled_strike"); + b.Property("Tags") + .HasColumnType("TEXT") + .HasColumnName("tags"); + b.HasKey("Id") .HasName("pk_apprise_configs"); diff --git a/code/backend/Cleanuparr.Persistence/Models/Configuration/Notification/AppriseConfig.cs b/code/backend/Cleanuparr.Persistence/Models/Configuration/Notification/AppriseConfig.cs index a3e6ad87..e5a6376b 100644 --- a/code/backend/Cleanuparr.Persistence/Models/Configuration/Notification/AppriseConfig.cs +++ b/code/backend/Cleanuparr.Persistence/Models/Configuration/Notification/AppriseConfig.cs @@ -11,6 +11,8 @@ public sealed record AppriseConfig : NotificationConfig public string? Key { get; set; } + public string? Tags { get; set; } + public override bool IsValid() { if (Url is null) diff --git a/code/frontend/src/app/settings/notification-settings/notification-settings.component.html b/code/frontend/src/app/settings/notification-settings/notification-settings.component.html index 72feb79a..6023bb73 100644 --- a/code/frontend/src/app/settings/notification-settings/notification-settings.component.html +++ b/code/frontend/src/app/settings/notification-settings/notification-settings.component.html @@ -53,10 +53,7 @@ title="Click for documentation"> Channel ID - - +
@@ -139,6 +136,21 @@ The key of your Apprise config
+ + +
+ +
+ + Optionally notify only those tagged accordingly. Use a comma (,) to OR your tags and a space ( ) to AND them. +
+
diff --git a/code/frontend/src/app/settings/notification-settings/notification-settings.component.ts b/code/frontend/src/app/settings/notification-settings/notification-settings.component.ts index d6611219..f04063a8 100644 --- a/code/frontend/src/app/settings/notification-settings/notification-settings.component.ts +++ b/code/frontend/src/app/settings/notification-settings/notification-settings.component.ts @@ -86,6 +86,7 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea apprise: this.formBuilder.group({ fullUrl: [''], key: [''], + tags: [''], onFailedImportStrike: [false], onStalledStrike: [false], onSlowStrike: [false], @@ -114,6 +115,7 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea apprise: config.apprise || { fullUrl: '', key: '', + tags: '', onFailedImportStrike: false, onStalledStrike: false, onSlowStrike: false, @@ -270,6 +272,7 @@ export class NotificationSettingsComponent implements OnDestroy, CanComponentDea apprise: { fullUrl: '', key: '', + tags: '', onFailedImportStrike: false, onStalledStrike: false, onSlowStrike: false, diff --git a/code/frontend/src/app/shared/models/apprise-config.model.ts b/code/frontend/src/app/shared/models/apprise-config.model.ts index ddb7f449..a2dede03 100644 --- a/code/frontend/src/app/shared/models/apprise-config.model.ts +++ b/code/frontend/src/app/shared/models/apprise-config.model.ts @@ -3,4 +3,5 @@ import { NotificationConfig } from './notification-config.model'; export interface AppriseConfig extends NotificationConfig { fullUrl?: string; key?: string; + tags?: string; } diff --git a/docs/docs/configuration/notifications/index.mdx b/docs/docs/configuration/notifications/index.mdx index 2c7bd31e..12dc386e 100644 --- a/docs/docs/configuration/notifications/index.mdx +++ b/docs/docs/configuration/notifications/index.mdx @@ -83,6 +83,16 @@ The key that identifies your Apprise configuration. This corresponds to a config + + +Optionally notify only those tagged accordingly. Use a comma (,) to OR your tags and a space ( ) to AND them. More details on this can be seen in the [Apprise documentation](https://github.com/caronc/apprise-api?tab=readme-ov-file#tagging). + + +