parent
bd3b398e38
commit
eb4102fa22
@ -1,25 +1,42 @@
|
||||
using Recyclarr.Cli.Console.Settings;
|
||||
using Recyclarr.Cli.Pipelines;
|
||||
using Recyclarr.Config.Models;
|
||||
using Recyclarr.Notifications;
|
||||
|
||||
namespace Recyclarr.Cli.Processors.Sync;
|
||||
|
||||
public class SyncPipelineExecutor(
|
||||
ILogger log,
|
||||
IOrderedEnumerable<ISyncPipeline> pipelines,
|
||||
IEnumerable<IPipelineCache> caches)
|
||||
IEnumerable<IPipelineCache> caches,
|
||||
NotificationEmitter emitter)
|
||||
{
|
||||
public async Task Process(ISyncSettings settings, IServiceConfiguration config)
|
||||
{
|
||||
foreach (var cache in caches)
|
||||
try
|
||||
{
|
||||
cache.Clear();
|
||||
}
|
||||
emitter.NotifyStatistic("Test statistic", "10");
|
||||
emitter.NotifyStatistic("Test statistic 2", "1060");
|
||||
emitter.NotifyError("Failure occurred");
|
||||
emitter.NotifyError("Another failure occurred");
|
||||
emitter.NotifyError("Another failure occurred 2");
|
||||
foreach (var cache in caches)
|
||||
{
|
||||
cache.Clear();
|
||||
}
|
||||
|
||||
foreach (var pipeline in pipelines)
|
||||
{
|
||||
log.Debug("Executing Pipeline: {Pipeline}", pipeline.GetType().Name);
|
||||
await pipeline.Execute(settings, config);
|
||||
}
|
||||
|
||||
foreach (var pipeline in pipelines)
|
||||
log.Information("Completed at {Date}", DateTime.Now);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Debug("Executing Pipeline: {Pipeline}", pipeline.GetType().Name);
|
||||
await pipeline.Execute(settings, config);
|
||||
emitter.NotifyError($"Exception: {e.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
using Flurl.Http;
|
||||
using Recyclarr.Notifications.Apprise.Dto;
|
||||
using Recyclarr.Settings;
|
||||
|
||||
namespace Recyclarr.Notifications.Apprise;
|
||||
|
||||
public class AppriseNotificationApiService(IAppriseRequestBuilder api, ISettingsProvider settingsProvider)
|
||||
: IAppriseNotificationApiService
|
||||
{
|
||||
public async Task Notify(string key, AppriseNotification notification)
|
||||
{
|
||||
var settings = settingsProvider.Settings.Notifications?.Apprise;
|
||||
if (settings?.Key is null)
|
||||
{
|
||||
throw new ArgumentException("No apprise notification settings have been defined");
|
||||
}
|
||||
|
||||
await api.Request("notify", settings.Key)
|
||||
.PostJsonAsync(notification);
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Flurl.Http;
|
||||
using Flurl.Http.Configuration;
|
||||
using Recyclarr.Http;
|
||||
using Recyclarr.Settings;
|
||||
|
||||
namespace Recyclarr.Notifications.Apprise;
|
||||
|
||||
public sealed class AppriseRequestBuilder(
|
||||
IFlurlClientCache clientCache,
|
||||
ISettingsProvider settingsProvider,
|
||||
IEnumerable<FlurlSpecificEventHandler> eventHandlers)
|
||||
: IAppriseRequestBuilder
|
||||
{
|
||||
private readonly Lazy<Uri> _baseUrl = new(() =>
|
||||
{
|
||||
var settings = settingsProvider.Settings.Notifications?.Apprise;
|
||||
if (settings is null)
|
||||
{
|
||||
throw new ArgumentException("No apprise notification settings have been defined");
|
||||
}
|
||||
|
||||
if (settings.BaseUrl is null)
|
||||
{
|
||||
throw new ArgumentException("Apprise `base_url` setting is not present or empty");
|
||||
}
|
||||
|
||||
return settings.BaseUrl;
|
||||
});
|
||||
|
||||
public IFlurlRequest Request(params object[] path)
|
||||
{
|
||||
var client = clientCache.GetOrAdd("apprise", _baseUrl.Value.ToString(), Configure);
|
||||
return client.Request(path);
|
||||
}
|
||||
|
||||
private void Configure(IFlurlClientBuilder builder)
|
||||
{
|
||||
foreach (var handler in eventHandlers.Select(x => (x.EventType, x)))
|
||||
{
|
||||
builder.EventHandlers.Add(handler);
|
||||
}
|
||||
|
||||
builder.WithSettings(settings =>
|
||||
{
|
||||
settings.JsonSerializer = new DefaultJsonSerializer(new JsonSerializerOptions
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
Converters =
|
||||
{
|
||||
new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace Recyclarr.Notifications.Apprise.Dto;
|
||||
|
||||
public enum AppriseMessageFormat
|
||||
{
|
||||
Text,
|
||||
Markdown,
|
||||
Html
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace Recyclarr.Notifications.Apprise.Dto;
|
||||
|
||||
public enum AppriseMessageType
|
||||
{
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Failure
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
namespace Recyclarr.Notifications.Apprise.Dto;
|
||||
|
||||
public record AppriseNotification
|
||||
{
|
||||
public required string Body { get; init; }
|
||||
public string? Title { get; init; }
|
||||
public AppriseMessageType? Type { get; init; }
|
||||
public AppriseMessageFormat? Format { get; init; }
|
||||
public string? Tag { get; init; }
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Recyclarr.Notifications.Apprise.Dto;
|
||||
|
||||
namespace Recyclarr.Notifications.Apprise;
|
||||
|
||||
public interface IAppriseNotificationApiService
|
||||
{
|
||||
Task Notify(string key, AppriseNotification notification);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Flurl.Http;
|
||||
|
||||
namespace Recyclarr.Notifications.Apprise;
|
||||
|
||||
public interface IAppriseRequestBuilder
|
||||
{
|
||||
IFlurlRequest Request(params object[] path);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Recyclarr.Notifications.Events;
|
||||
|
||||
public record ErrorEvent(string Error) : INotificationEvent
|
||||
{
|
||||
public string Category => "Errors";
|
||||
public string Render() => $"- {Error}";
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Recyclarr.Notifications.Events;
|
||||
|
||||
public interface INotificationEvent
|
||||
{
|
||||
public string Category { get; }
|
||||
public string Render();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Recyclarr.Notifications.Events;
|
||||
|
||||
public record StatisticEvent(string Description, string Statistic) : INotificationEvent
|
||||
{
|
||||
public string Category => "Statistics";
|
||||
public string Render() => $"- {Description}: {Statistic}";
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using Recyclarr.Notifications.Events;
|
||||
|
||||
namespace Recyclarr.Notifications;
|
||||
|
||||
public class NotificationEmitter
|
||||
{
|
||||
private readonly Subject<INotificationEvent> _notifications = new();
|
||||
|
||||
public IObservable<INotificationEvent> OnNotification => _notifications.AsObservable();
|
||||
|
||||
public void NotifyStatistic(string description, string stat)
|
||||
{
|
||||
_notifications.OnNext(new StatisticEvent(description, stat));
|
||||
}
|
||||
|
||||
public void NotifyError(string error)
|
||||
{
|
||||
_notifications.OnNext(new ErrorEvent(error));
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
using System.Reactive.Disposables;
|
||||
using System.Text;
|
||||
using Flurl.Http;
|
||||
using Recyclarr.Common.Extensions;
|
||||
using Recyclarr.Http;
|
||||
using Recyclarr.Notifications.Apprise;
|
||||
using Recyclarr.Notifications.Apprise.Dto;
|
||||
using Recyclarr.Notifications.Events;
|
||||
using Recyclarr.Settings;
|
||||
using Serilog;
|
||||
|
||||
namespace Recyclarr.Notifications;
|
||||
|
||||
public sealed class NotificationService(
|
||||
ILogger log,
|
||||
IAppriseNotificationApiService apprise,
|
||||
ISettingsProvider settingsProvider,
|
||||
NotificationEmitter notificationEmitter) : IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, List<INotificationEvent>> _events = new();
|
||||
private readonly CompositeDisposable _eventConnection = new();
|
||||
private string? _activeInstanceName;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_eventConnection.Dispose();
|
||||
}
|
||||
|
||||
public void SetInstanceName(string instanceName)
|
||||
{
|
||||
_activeInstanceName = instanceName;
|
||||
}
|
||||
|
||||
public void BeginWatchEvents()
|
||||
{
|
||||
_events.Clear();
|
||||
_eventConnection.Clear();
|
||||
_eventConnection.Add(notificationEmitter.OnNotification.Subscribe(x =>
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(_activeInstanceName);
|
||||
_events.GetOrCreate(_activeInstanceName).Add(x);
|
||||
}));
|
||||
}
|
||||
|
||||
public async Task SendNotification(bool succeeded)
|
||||
{
|
||||
// stop receiving events while we build the report
|
||||
_eventConnection.Clear();
|
||||
|
||||
// If the user didn't configure notifications, exit early and do nothing.
|
||||
if (settingsProvider.Settings.Notifications is null)
|
||||
{
|
||||
log.Debug("Notification settings are not present, so this notification will not be sent");
|
||||
return;
|
||||
}
|
||||
|
||||
var body = new StringBuilder();
|
||||
|
||||
foreach (var (instanceName, notifications) in _events)
|
||||
{
|
||||
RenderInstanceEvents(body, instanceName, notifications);
|
||||
}
|
||||
|
||||
var messageType = AppriseMessageType.Success;
|
||||
if (!succeeded)
|
||||
{
|
||||
messageType = AppriseMessageType.Failure;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await apprise.Notify("apprise", new AppriseNotification
|
||||
{
|
||||
Title = $"Recyclarr Sync {(succeeded ? "Completed" : "Failed")}",
|
||||
Body = body.ToString(),
|
||||
Type = messageType,
|
||||
Format = AppriseMessageFormat.Markdown
|
||||
});
|
||||
}
|
||||
catch (FlurlHttpException e)
|
||||
{
|
||||
log.Error("Failed to send notification: {Msg}", e.SanitizedExceptionMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void RenderInstanceEvents(
|
||||
StringBuilder body,
|
||||
string instanceName,
|
||||
IEnumerable<INotificationEvent> notifications)
|
||||
{
|
||||
body.AppendLine($"### Instance: `{instanceName}`");
|
||||
|
||||
var groupedEvents = notifications
|
||||
.GroupBy(x => x.Category)
|
||||
.ToDictionary(x => x.Key, x => x.ToList());
|
||||
|
||||
foreach (var (category, events) in groupedEvents)
|
||||
{
|
||||
body.AppendLine(
|
||||
$"""
|
||||
{category}:
|
||||
{string.Join('\n', events.Select(x => x.Render()))}
|
||||
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using Autofac;
|
||||
using Recyclarr.Notifications.Apprise;
|
||||
|
||||
namespace Recyclarr.Notifications;
|
||||
|
||||
public class NotificationsAutofacModule : Module
|
||||
{
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
base.Load(builder);
|
||||
|
||||
builder.RegisterType<NotificationService>().InstancePerLifetimeScope();
|
||||
builder.RegisterType<NotificationEmitter>().InstancePerLifetimeScope();
|
||||
|
||||
// Apprise
|
||||
builder.RegisterType<AppriseNotificationApiService>().As<IAppriseNotificationApiService>();
|
||||
builder.RegisterType<AppriseRequestBuilder>().As<IAppriseRequestBuilder>();
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Flurl.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Recyclarr.Http\Recyclarr.Http.csproj" />
|
||||
<ProjectReference Include="..\Recyclarr.Settings\Recyclarr.Settings.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
Loading…
Reference in new issue