diff --git a/README.md b/README.md index 9c37dfce..d56873c3 100644 --- a/README.md +++ b/README.md @@ -291,6 +291,14 @@ services: - NOTIFIARR__ON_DOWNLOAD_CLEANED=true - NOTIFIARR__API_KEY=notifiarr_secret - NOTIFIARR__CHANNEL_ID=discord_channel_id + + - APPRISE__ON_IMPORT_FAILED_STRIKE=true + - APPRISE__ON_STALLED_STRIKE=true + - APPRISE__ON_SLOW_STRIKE=true + - APPRISE__ON_QUEUE_ITEM_DELETED=true + - APPRISE__ON_DOWNLOAD_CLEANED=true + - APPRISE__URL=http://apprise:8000 + - APPRISE__KEY=myConfigKey ``` ### Windows diff --git a/code/Executable/DependencyInjection/NotificationsDI.cs b/code/Executable/DependencyInjection/NotificationsDI.cs index df9961b1..54b7ddc6 100644 --- a/code/Executable/DependencyInjection/NotificationsDI.cs +++ b/code/Executable/DependencyInjection/NotificationsDI.cs @@ -1,4 +1,5 @@ using Infrastructure.Verticals.Notifications; +using Infrastructure.Verticals.Notifications.Apprise; using Infrastructure.Verticals.Notifications.Notifiarr; namespace Executable.DependencyInjection; @@ -8,8 +9,11 @@ public static class NotificationsDI public static IServiceCollection AddNotifications(this IServiceCollection services, IConfiguration configuration) => services .Configure(configuration.GetSection(NotifiarrConfig.SectionName)) + .Configure(configuration.GetSection(AppriseConfig.SectionName)) .AddTransient() .AddTransient() + .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient(); diff --git a/code/Executable/appsettings.Development.json b/code/Executable/appsettings.Development.json index e70be6e7..872be2c9 100644 --- a/code/Executable/appsettings.Development.json +++ b/code/Executable/appsettings.Development.json @@ -122,5 +122,14 @@ "ON_DOWNLOAD_CLEANED": true, "API_KEY": "", "CHANNEL_ID": "" + }, + "Apprise": { + "ON_IMPORT_FAILED_STRIKE": true, + "ON_STALLED_STRIKE": true, + "ON_SLOW_STRIKE": true, + "ON_QUEUE_ITEM_DELETED": true, + "ON_DOWNLOAD_CLEANED": true, + "URL": "http://localhost:8000", + "KEY": "" } } diff --git a/code/Executable/appsettings.json b/code/Executable/appsettings.json index 1dc52479..3158a02c 100644 --- a/code/Executable/appsettings.json +++ b/code/Executable/appsettings.json @@ -105,5 +105,14 @@ "ON_DOWNLOAD_CLEANED": false, "API_KEY": "", "CHANNEL_ID": "" + }, + "Apprise": { + "ON_IMPORT_FAILED_STRIKE": false, + "ON_STALLED_STRIKE": false, + "ON_SLOW_STRIKE": false, + "ON_QUEUE_ITEM_DELETED": false, + "ON_DOWNLOAD_CLEANED": false, + "URL": "", + "KEY": "" } } diff --git a/code/Infrastructure/Verticals/Notifications/Apprise/AppriseConfig.cs b/code/Infrastructure/Verticals/Notifications/Apprise/AppriseConfig.cs new file mode 100644 index 00000000..fd06c631 --- /dev/null +++ b/code/Infrastructure/Verticals/Notifications/Apprise/AppriseConfig.cs @@ -0,0 +1,27 @@ +using Common.Configuration.Notification; + +namespace Infrastructure.Verticals.Notifications.Apprise; + +public sealed record AppriseConfig : NotificationConfig +{ + public const string SectionName = "Apprise"; + + public Uri? Url { get; init; } + + public string? Key { get; init; } + + public override bool IsValid() + { + if (Url is null) + { + return false; + } + + if (string.IsNullOrEmpty(Key?.Trim())) + { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Notifications/Apprise/ApprisePayload.cs b/code/Infrastructure/Verticals/Notifications/Apprise/ApprisePayload.cs new file mode 100644 index 00000000..74804297 --- /dev/null +++ b/code/Infrastructure/Verticals/Notifications/Apprise/ApprisePayload.cs @@ -0,0 +1,31 @@ +using System.ComponentModel.DataAnnotations; + +namespace Infrastructure.Verticals.Notifications.Apprise; + +public sealed record ApprisePayload +{ + [Required] + public string Title { get; init; } + + [Required] + public string Body { get; init; } + + public string Type { get; init; } = NotificationType.Info.ToString().ToLowerInvariant(); + + public string Format { get; init; } = FormatType.Text.ToString().ToLowerInvariant(); +} + +public enum NotificationType +{ + Info, + Success, + Warning, + Failure +} + +public enum FormatType +{ + Text, + Markdown, + Html +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Notifications/Apprise/AppriseProvider.cs b/code/Infrastructure/Verticals/Notifications/Apprise/AppriseProvider.cs new file mode 100644 index 00000000..10384db3 --- /dev/null +++ b/code/Infrastructure/Verticals/Notifications/Apprise/AppriseProvider.cs @@ -0,0 +1,90 @@ +using System.Text; +using Infrastructure.Verticals.Notifications.Models; +using Microsoft.Extensions.Options; + +namespace Infrastructure.Verticals.Notifications.Apprise; + +public sealed class AppriseProvider : NotificationProvider +{ + private readonly AppriseConfig _config; + private readonly IAppriseProxy _proxy; + + public AppriseProvider(IOptions config, IAppriseProxy proxy) + : base(config) + { + _config = config.Value; + _proxy = proxy; + } + + public override string Name => "Apprise"; + + public override async Task OnFailedImportStrike(FailedImportStrikeNotification notification) + { + await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config); + } + + public override async Task OnStalledStrike(StalledStrikeNotification notification) + { + await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config); + } + + public override async Task OnSlowStrike(SlowStrikeNotification notification) + { + await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config); + } + + public override async Task OnQueueItemDeleted(QueueItemDeletedNotification notification) + { + await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config); + } + + public override async Task OnDownloadCleaned(DownloadCleanedNotification notification) + { + await _proxy.SendNotification(BuildPayload(notification, NotificationType.Warning), _config); + } + + private static ApprisePayload BuildPayload(ArrNotification notification, NotificationType notificationType) + { + StringBuilder body = new(); + body.AppendLine(notification.Description); + body.AppendLine(); + body.AppendLine($"Instance type: {notification.InstanceType.ToString()}"); + body.AppendLine($"Url: {notification.InstanceUrl}"); + body.AppendLine($"Download hash: {notification.Hash}"); + + foreach (NotificationField field in notification.Fields ?? []) + { + body.AppendLine($"{field.Title}: {field.Text}"); + } + + ApprisePayload payload = new() + { + Title = notification.Title, + Body = body.ToString(), + Type = notificationType.ToString().ToLowerInvariant(), + }; + + return payload; + } + + private static ApprisePayload BuildPayload(Notification notification, NotificationType notificationType) + { + StringBuilder body = new(); + body.AppendLine(notification.Description); + body.AppendLine(); + + foreach (NotificationField field in notification.Fields ?? []) + { + body.AppendLine($"{field.Title}: {field.Text}"); + } + + ApprisePayload payload = new() + { + Title = notification.Title, + Body = body.ToString(), + Type = notificationType.ToString().ToLowerInvariant(), + }; + + return payload; + } +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Notifications/Apprise/AppriseProxy.cs b/code/Infrastructure/Verticals/Notifications/Apprise/AppriseProxy.cs new file mode 100644 index 00000000..941fcb56 --- /dev/null +++ b/code/Infrastructure/Verticals/Notifications/Apprise/AppriseProxy.cs @@ -0,0 +1,34 @@ +using System.Text; +using Common.Helpers; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Infrastructure.Verticals.Notifications.Apprise; + +public sealed class AppriseProxy : IAppriseProxy +{ + private readonly HttpClient _httpClient; + + public AppriseProxy(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName); + } + + public async Task SendNotification(ApprisePayload payload, AppriseConfig config) + { + string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }); + + UriBuilder uriBuilder = new(config.Url); + 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"); + + using HttpResponseMessage response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Notifications/Apprise/IAppriseProxy.cs b/code/Infrastructure/Verticals/Notifications/Apprise/IAppriseProxy.cs new file mode 100644 index 00000000..534aa594 --- /dev/null +++ b/code/Infrastructure/Verticals/Notifications/Apprise/IAppriseProxy.cs @@ -0,0 +1,6 @@ +namespace Infrastructure.Verticals.Notifications.Apprise; + +public interface IAppriseProxy +{ + Task SendNotification(ApprisePayload payload, AppriseConfig config); +} \ No newline at end of file diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProvider.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProvider.cs index 66775d77..9a278774 100644 --- a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProvider.cs +++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProvider.cs @@ -12,6 +12,8 @@ public class NotifiarrProvider : NotificationProvider private const string WarningColor = "f0ad4e"; private const string ImportantColor = "bb2124"; private const string Logo = "https://github.com/flmorg/cleanuperr/blob/main/Logo/48.png?raw=true"; + + public override string Name => "Notifiarr"; public NotifiarrProvider(IOptions config, INotifiarrProxy proxy) : base(config) @@ -20,8 +22,6 @@ public class NotifiarrProvider : NotificationProvider _proxy = proxy; } - public override string Name => "Notifiarr"; - public override async Task OnFailedImportStrike(FailedImportStrikeNotification notification) { await _proxy.SendNotification(BuildPayload(notification, WarningColor), _config); diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProxy.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProxy.cs index edfc01a1..f41c4ed6 100644 --- a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProxy.cs +++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProxy.cs @@ -5,7 +5,7 @@ using Newtonsoft.Json.Serialization; namespace Infrastructure.Verticals.Notifications.Notifiarr; -public class NotifiarrProxy : INotifiarrProxy +public sealed class NotifiarrProxy : INotifiarrProxy { private readonly HttpClient _httpClient; diff --git a/code/test/docker-compose.yml b/code/test/docker-compose.yml index f249d236..0f031a9f 100644 --- a/code/test/docker-compose.yml +++ b/code/test/docker-compose.yml @@ -262,13 +262,21 @@ services: - LIDARR__INSTANCES__0__URL=http://lidarr:8686 - LIDARR__INSTANCES__0__APIKEY=7f677cfdc074414397af53dd633860c5 - - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true - - NOTIFIARR__ON_STALLED_STRIKE=true - - NOTIFIARR__ON_SLOW_STRIKE=true - - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true - - NOTIFIARR__ON_DOWNLOAD_CLEANED=true - - NOTIFIARR__API_KEY=notifiarr_secret - - NOTIFIARR__CHANNEL_ID=discord_channel_id + # - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=true + # - NOTIFIARR__ON_STALLED_STRIKE=true + # - NOTIFIARR__ON_SLOW_STRIKE=true + # - NOTIFIARR__ON_QUEUE_ITEM_DELETED=true + # - NOTIFIARR__ON_DOWNLOAD_CLEANED=true + # - NOTIFIARR__API_KEY=notifiarr_secret + # - NOTIFIARR__CHANNEL_ID=discord_channel_id + + # - APPRISE__ON_IMPORT_FAILED_STRIKE=true + # - APPRISE__ON_STALLED_STRIKE=true + # - APPRISE__ON_SLOW_STRIKE=true + # - APPRISE__ON_QUEUE_ITEM_DELETED=true + # - APPRISE__ON_DOWNLOAD_CLEANED=true + # - APPRISE__URL=http://localhost:8000 + # - APPRISE__KEY=mykey volumes: - ./data/cleanuperr/logs:/var/logs - ./data/cleanuperr/ignored_downloads:/ignored diff --git a/variables.md b/variables.md index fa3a0302..8ee7937e 100644 --- a/variables.md +++ b/variables.md @@ -6,6 +6,8 @@ - [Download Client settings](#download-client-settings) - [Arr settings](#arr-settings) - [Notification settings](#notification-settings) + - [Notifiarr settings](#notifiarr__api_key) + - [Apprise settings](#apprise__url) - [Advanced settings](#advanced-settings) # @@ -702,6 +704,53 @@ - Default: `false` - Required: No. +#### **`APPRISE__URL`** +- [Apprise url](https://github.com/caronc/apprise-api) where to send notifications. +- Type: String +- Default: Empty. +- Required: No. + +#### **`APPRISE__KEY`** +- [Apprise configuration key](https://github.com/caronc/apprise-api?tab=readme-ov-file#screenshots) containing all 3rd party notification providers which Cleanuperr would notify. +- Type: String +- Default: Empty. +- Required: No. + +#### **`APPRISE__ON_IMPORT_FAILED_STRIKE`** +- Controls whether to notify when an item receives a failed import strike. +- Type: Boolean +- Possible values: `true`, `false` +- Default: `false` +- Required: No. + +#### **`APPRISE__ON_STALLED_STRIKE`** +- Controls whether to notify when an item receives a stalled download strike. This includes strikes for being stuck while downloading metadata. +- Type: Boolean +- Possible values: `true`, `false` +- Default: `false` +- Required: No. + +#### **`APPRISE__ON_SLOW_STRIKE`** +- Controls whether to notify when an item receives a slow download strike. This includes strikes for having a low download speed or slow estimated finish time. +- Type: Boolean +- Possible values: `true`, `false` +- Default: `false` +- Required: No. + +#### **`APPRISE__ON_QUEUE_ITEM_DELETED`** +- Controls whether to notify when a queue item is deleted. +- Type: Boolean +- Possible values: `true`, `false` +- Default: `false` +- Required: No. + +#### **`APPRISE__ON_DOWNLOAD_CLEANED`** +- Controls whether to notify when a download is cleaned. +- Type: Boolean +- Possible values: `true`, `false` +- Default: `false` +- Required: No. + # ### Advanced settings