fixup! wip: Notifications support through Apprise

Robert Dailey 3 weeks ago
parent 3ab3ab895d
commit 8b501a6044

@ -3,6 +3,8 @@ using System.Reflection;
using Autofac;
using Autofac.Extras.Ordering;
using AutoMapper.Contrib.Autofac.DependencyInjection;
using MediatR.Extensions.Autofac.DependencyInjection;
using MediatR.Extensions.Autofac.DependencyInjection.Builder;
using Recyclarr.Cli.Cache;
using Recyclarr.Cli.Console.Setup;
using Recyclarr.Cli.Logging;
@ -43,6 +45,10 @@ public static class CompositionRoot
RegisterLogger(builder);
builder.RegisterMediatR(MediatRConfigurationBuilder.Create(thisAssembly)
.WithRequestHandlersManuallyRegistered()
.Build());
builder.RegisterModule<MigrationAutofacModule>();
builder.RegisterModule<ConfigAutofacModule>();
builder.RegisterModule<GuideAutofacModule>();

@ -23,7 +23,6 @@ public class SyncProcessor(
{
public async Task<ExitStatus> Process(ISyncSettings settings)
{
notify.BeginWatchEvents();
var result = await ProcessConfigs(settings);
await notify.SendNotification(result != ExitStatus.Failed);
return result;
@ -65,6 +64,7 @@ public class SyncProcessor(
{
try
{
// todo: Create new NotificationScope here; but how do we collect messages for each config instance we process? Should NotificationService be scoped as well?
notify.SetInstanceName(config.InstanceName);
PrintProcessingHeader(config.ServiceType, config);
await capabilityEnforcer.Check(config);

@ -9,6 +9,8 @@
<PackageReference Include="Autofac.Extras.AggregateService" />
<PackageReference Include="Autofac.Extras.Ordering" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="MediatR" />
<PackageReference Include="MediatR.Extensions.Autofac.DependencyInjection" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Expressions" />
<PackageReference Include="Serilog.Sinks.Console" />

@ -1,6 +1,6 @@
namespace Recyclarr.Notifications.Events;
public record ErrorEvent(string Error) : INotificationEvent
public record ErrorEvent(string Error) : IPresentableNotification
{
public string Category => "Errors";
public string Render() => $"- {Error}";

@ -1,6 +1,6 @@
namespace Recyclarr.Notifications.Events;
public interface INotificationEvent
public interface IPresentableNotification
{
public string Category { get; }
public string Render();

@ -4,17 +4,13 @@ namespace Recyclarr.Notifications.Events;
public record StatisticEvent(string Description, string Statistic) : IRequest;
public class StatisticEventHandler : INotificationEvent, IRequestHandler<StatisticEvent>
internal class StatisticEventHandler : IPresentableNotification, IRequestHandler<StatisticEvent>
{
public string Category => "Statistics";
public string Render() => string.Join('\n', _events.Select(x => $"- {x.Description}: {x.Statistic}"));
private readonly List<StatisticEvent> _events = [];
public StatisticEventHandler()
{
}
public Task Handle(StatisticEvent request, CancellationToken cancellationToken)
{
_events.Add(request);

@ -7,9 +7,9 @@ namespace Recyclarr.Notifications;
public class NotificationEmitter(IMediator mediator)
{
private readonly Subject<INotificationEvent> _notifications = new();
private readonly Subject<IPresentableNotification> _notifications = new();
public IObservable<INotificationEvent> OnNotification => _notifications.AsObservable();
public IObservable<IPresentableNotification> OnNotification => _notifications.AsObservable();
public void NotifyStatistic<T>(string description, T stat) where T : notnull
{

@ -0,0 +1,15 @@
using Autofac;
namespace Recyclarr.Notifications;
internal sealed class NotificationScope(ILifetimeScope scope) : IDisposable
{
public static string ScopeName => "notification";
private readonly ILifetimeScope _scope = scope.BeginLifetimeScope(ScopeName);
public void Dispose()
{
_scope.Dispose();
}
}

@ -1,7 +1,5 @@
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;
@ -15,38 +13,17 @@ public sealed class NotificationService(
ILogger log,
IAppriseNotificationApiService apprise,
ISettingsProvider settingsProvider,
NotificationEmitter notificationEmitter) : IDisposable
IReadOnlyCollection<IPresentableNotification> presentableNotifications)
{
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)
{
@ -54,12 +31,9 @@ public sealed class NotificationService(
return;
}
var body = new StringBuilder();
ArgumentException.ThrowIfNullOrWhiteSpace(_activeInstanceName);
foreach (var (instanceName, notifications) in _events)
{
RenderInstanceEvents(body, instanceName, notifications);
}
var body = RenderInstanceEvents(_activeInstanceName, presentableNotifications);
var messageType = AppriseMessageType.Success;
if (!succeeded)
@ -72,7 +46,7 @@ public sealed class NotificationService(
await apprise.Notify("apprise", new AppriseNotification
{
Title = $"Recyclarr Sync {(succeeded ? "Completed" : "Failed")}",
Body = body.ToString(),
Body = body,
Type = messageType,
Format = AppriseMessageFormat.Markdown
});
@ -83,25 +57,22 @@ public sealed class NotificationService(
}
}
private static void RenderInstanceEvents(
StringBuilder body,
private static string RenderInstanceEvents(
string instanceName,
IEnumerable<INotificationEvent> notifications)
IEnumerable<IPresentableNotification> notifications)
{
body.AppendLine($"### Instance: `{instanceName}`\n");
var groupedEvents = notifications
.GroupBy(x => x.Category)
.ToDictionary(x => x.Key, x => x.ToList());
var body = new StringBuilder($"### Instance: `{instanceName}`\n");
foreach (var (category, events) in groupedEvents)
foreach (var notification in notifications.GroupBy(x => x.Category))
{
body.AppendLine(
$"""
{category}:
{string.Join('\n', events.Select(x => x.Render()))}
{notification.Key}:
{string.Join('\n', notification.Select(x => x.Render()))}
""");
}
return body.ToString();
}
}

@ -1,6 +1,5 @@
using Autofac;
using MediatR.Extensions.Autofac.DependencyInjection;
using MediatR.Extensions.Autofac.DependencyInjection.Builder;
using MediatR;
using Recyclarr.Notifications.Apprise;
namespace Recyclarr.Notifications;
@ -11,12 +10,13 @@ public class NotificationsAutofacModule : Module
{
base.Load(builder);
builder.RegisterMediatR(MediatRConfigurationBuilder.Create(ThisAssembly)
.WithAllOpenGenericHandlerTypesRegistered()
.Build());
builder.RegisterAssemblyTypes(ThisAssembly)
.AsClosedTypesOf(typeof(IRequestHandler<>))
.AsImplementedInterfaces()
.InstancePerMatchingLifetimeScope(NotificationScope.ScopeName);
builder.RegisterType<NotificationService>().InstancePerLifetimeScope();
builder.RegisterType<NotificationEmitter>().InstancePerLifetimeScope();
builder.RegisterType<NotificationService>().InstancePerMatchingLifetimeScope(NotificationScope.ScopeName);
builder.RegisterType<NotificationEmitter>().InstancePerMatchingLifetimeScope(NotificationScope.ScopeName);
// Apprise
builder.RegisterType<AppriseNotificationApiService>().As<IAppriseNotificationApiService>();

@ -2,7 +2,6 @@
<ItemGroup>
<PackageReference Include="Flurl.Http" />
<PackageReference Include="MediatR" />
<PackageReference Include="MediatR.Extensions.Autofac.DependencyInjection" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Recyclarr.Http\Recyclarr.Http.csproj" />

Loading…
Cancel
Save