Compare commits

...

1 Commits

Author SHA1 Message Date
Flaminel
75b001cf6a Add Apprise support (#124) 2025-05-01 21:00:01 +03:00
13 changed files with 285 additions and 10 deletions

View File

@@ -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
```
### <img src="https://raw.githubusercontent.com/FortAwesome/Font-Awesome/6.x/svgs/brands/windows.svg" height="20" style="vertical-align: middle;"> <span style="vertical-align: middle;">Windows</span>

View File

@@ -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<NotifiarrConfig>(configuration.GetSection(NotifiarrConfig.SectionName))
.Configure<AppriseConfig>(configuration.GetSection(AppriseConfig.SectionName))
.AddTransient<INotifiarrProxy, NotifiarrProxy>()
.AddTransient<INotificationProvider, NotifiarrProvider>()
.AddTransient<IAppriseProxy, AppriseProxy>()
.AddTransient<INotificationProvider, AppriseProvider>()
.AddTransient<INotificationPublisher, NotificationPublisher>()
.AddTransient<INotificationFactory, NotificationFactory>()
.AddTransient<NotificationService>();

View File

@@ -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": ""
}
}

View File

@@ -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": ""
}
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -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<AppriseConfig> 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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,6 @@
namespace Infrastructure.Verticals.Notifications.Apprise;
public interface IAppriseProxy
{
Task SendNotification(ApprisePayload payload, AppriseConfig config);
}

View File

@@ -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<NotifiarrConfig> 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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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