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