parent
bd3b398e38
commit
eb4102fa22
@ -1,25 +1,42 @@
|
|||||||
using Recyclarr.Cli.Console.Settings;
|
using Recyclarr.Cli.Console.Settings;
|
||||||
using Recyclarr.Cli.Pipelines;
|
using Recyclarr.Cli.Pipelines;
|
||||||
using Recyclarr.Config.Models;
|
using Recyclarr.Config.Models;
|
||||||
|
using Recyclarr.Notifications;
|
||||||
|
|
||||||
namespace Recyclarr.Cli.Processors.Sync;
|
namespace Recyclarr.Cli.Processors.Sync;
|
||||||
|
|
||||||
public class SyncPipelineExecutor(
|
public class SyncPipelineExecutor(
|
||||||
ILogger log,
|
ILogger log,
|
||||||
IOrderedEnumerable<ISyncPipeline> pipelines,
|
IOrderedEnumerable<ISyncPipeline> pipelines,
|
||||||
IEnumerable<IPipelineCache> caches)
|
IEnumerable<IPipelineCache> caches,
|
||||||
|
NotificationEmitter emitter)
|
||||||
{
|
{
|
||||||
public async Task Process(ISyncSettings settings, IServiceConfiguration config)
|
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);
|
emitter.NotifyError($"Exception: {e.Message}");
|
||||||
await pipeline.Execute(settings, config);
|
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