diff --git a/README.md b/README.md
index c5bbe45d..2ce667ed 100644
--- a/README.md
+++ b/README.md
@@ -178,6 +178,12 @@ services:
- LIDARR__INSTANCES__0__APIKEY=secret5
- LIDARR__INSTANCES__1__URL=http://radarr:8687
- LIDARR__INSTANCES__1__APIKEY=secret6
+
+ # - NOTIFIARR__ON_IMPORT_FAILED_STRIKE=false
+ # - NOTIFIARR__ON_STALLED_STRIKE=false
+ # - NOTIFIARR__ON_QUEUE_ITEM_DELETE=false
+ # - NOTIFIARR__API_KEY=notifiarr_secret
+ # - NOTIFIARR__CHANNEL_ID=discord_channel_id
```
## Environment variables
@@ -257,6 +263,20 @@ services:
| LIDARR__INSTANCES__0__APIKEY | No | First LIDARR instance API key | empty |
+### Notifications variables
+
+ Click here
+
+| Variable | Required | Description | Default value |
+|---|---|---|---|
+| NOTIFIARR__ON_IMPORT_FAILED_STRIKE | No | Notify on failed import strike. | false |
+| NOTIFIARR__ON_STALLED_STRIKE | No | Notify on stalled download strike. | false |
+| NOTIFIARR__ON_QUEUE_ITEM_DELETE | No | Notify on deleting a queue item. | false |
+| NOTIFIARR__API_KEY | No | Notifiarr API key.
Requires Notifiarr's `Passthrough` integration to work. | empty |
+| NOTIFIARR__CHANNEL_ID | No | Discord channel id for notifications. | empty |
+
+
+
### Advanced variables
Click here
diff --git a/code/Common/Configuration/Notification/NotificationConfig.cs b/code/Common/Configuration/Notification/NotificationConfig.cs
new file mode 100644
index 00000000..2fc4574b
--- /dev/null
+++ b/code/Common/Configuration/Notification/NotificationConfig.cs
@@ -0,0 +1,19 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Common.Configuration.Notification;
+
+public abstract record NotificationConfig
+{
+ [ConfigurationKeyName("ON_IMPORT_FAILED_STRIKE")]
+ public bool OnImportFailedStrike { get; init; }
+
+ [ConfigurationKeyName("ON_STALLED_STRIKE")]
+ public bool OnStalledStrike { get; init; }
+
+ [ConfigurationKeyName("ON_QUEUE_ITEM_DELETE")]
+ public bool OnQueueItemDelete { get; init; }
+
+ public bool IsEnabled => OnImportFailedStrike || OnStalledStrike || OnQueueItemDelete;
+
+ public abstract bool IsValid();
+}
\ No newline at end of file
diff --git a/code/Domain/Enums/DeleteReason.cs b/code/Domain/Enums/DeleteReason.cs
new file mode 100644
index 00000000..e46cdd6d
--- /dev/null
+++ b/code/Domain/Enums/DeleteReason.cs
@@ -0,0 +1,8 @@
+namespace Domain.Enums;
+
+public enum DeleteReason
+{
+ Stalled,
+ ImportFailed,
+ AllFilesBlocked
+}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Queue/Image.cs b/code/Domain/Models/Arr/Queue/Image.cs
new file mode 100644
index 00000000..5f7b3300
--- /dev/null
+++ b/code/Domain/Models/Arr/Queue/Image.cs
@@ -0,0 +1,8 @@
+namespace Domain.Models.Arr.Queue;
+
+public record Image
+{
+ public required string CoverType { get; init; }
+
+ public required Uri RemoteUrl { get; init; }
+}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Queue/LidarrImage.cs b/code/Domain/Models/Arr/Queue/LidarrImage.cs
new file mode 100644
index 00000000..b317d7e1
--- /dev/null
+++ b/code/Domain/Models/Arr/Queue/LidarrImage.cs
@@ -0,0 +1,8 @@
+namespace Domain.Models.Arr.Queue;
+
+public record LidarrImage
+{
+ public required string CoverType { get; init; }
+
+ public required Uri Url { get; init; }
+}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Queue/QueueAlbum.cs b/code/Domain/Models/Arr/Queue/QueueAlbum.cs
new file mode 100644
index 00000000..f006b172
--- /dev/null
+++ b/code/Domain/Models/Arr/Queue/QueueAlbum.cs
@@ -0,0 +1,6 @@
+namespace Domain.Models.Arr.Queue;
+
+public sealed record QueueAlbum
+{
+ public List Images { get; init; } = [];
+}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Queue/QueueMovie.cs b/code/Domain/Models/Arr/Queue/QueueMovie.cs
new file mode 100644
index 00000000..990d2e91
--- /dev/null
+++ b/code/Domain/Models/Arr/Queue/QueueMovie.cs
@@ -0,0 +1,6 @@
+namespace Domain.Models.Arr.Queue;
+
+public sealed record QueueMovie
+{
+ public List Images { get; init; } = [];
+}
\ No newline at end of file
diff --git a/code/Domain/Models/Arr/Queue/QueueRecord.cs b/code/Domain/Models/Arr/Queue/QueueRecord.cs
index c66f5980..0d98f930 100644
--- a/code/Domain/Models/Arr/Queue/QueueRecord.cs
+++ b/code/Domain/Models/Arr/Queue/QueueRecord.cs
@@ -7,14 +7,20 @@ public sealed record QueueRecord
public long EpisodeId { get; init; }
public long SeasonNumber { get; init; }
+ public QueueSeries? Series { get; init; }
+
// Radarr
public long MovieId { get; init; }
+ public QueueSeries? Movie { get; init; }
+
// Lidarr
public long ArtistId { get; init; }
public long AlbumId { get; init; }
+ public QueueAlbum? Album { get; init; }
+
// common
public required string Title { get; init; }
public string Status { get; init; }
diff --git a/code/Domain/Models/Arr/Queue/QueueSeries.cs b/code/Domain/Models/Arr/Queue/QueueSeries.cs
new file mode 100644
index 00000000..544a0a98
--- /dev/null
+++ b/code/Domain/Models/Arr/Queue/QueueSeries.cs
@@ -0,0 +1,6 @@
+namespace Domain.Models.Arr.Queue;
+
+public sealed record QueueSeries
+{
+ public List Images { get; init; } = [];
+}
\ No newline at end of file
diff --git a/code/Executable/DependencyInjection/LoggingDI.cs b/code/Executable/DependencyInjection/LoggingDI.cs
index 9af11687..dc33821c 100644
--- a/code/Executable/DependencyInjection/LoggingDI.cs
+++ b/code/Executable/DependencyInjection/LoggingDI.cs
@@ -63,6 +63,7 @@ public static class LoggingDI
Log.Logger = logConfig
.MinimumLevel.Is(level)
+ .MinimumLevel.Override("MassTransit", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.Extensions.Http", LogEventLevel.Warning)
.MinimumLevel.Override("Quartz", LogEventLevel.Warning)
diff --git a/code/Executable/DependencyInjection/MainDI.cs b/code/Executable/DependencyInjection/MainDI.cs
index cdfe8496..7e6d531f 100644
--- a/code/Executable/DependencyInjection/MainDI.cs
+++ b/code/Executable/DependencyInjection/MainDI.cs
@@ -2,6 +2,9 @@
using Common.Configuration.General;
using Common.Helpers;
using Infrastructure.Verticals.DownloadClient.Deluge;
+using Infrastructure.Verticals.Notifications.Consumers;
+using Infrastructure.Verticals.Notifications.Models;
+using MassTransit;
using Polly;
using Polly.Extensions.Http;
@@ -16,7 +19,26 @@ public static class MainDI
.AddConfiguration(configuration)
.AddMemoryCache()
.AddServices()
- .AddQuartzServices(configuration);
+ .AddQuartzServices(configuration)
+ .AddNotifications(configuration)
+ .AddMassTransit(config =>
+ {
+ config.AddConsumer>();
+ config.AddConsumer>();
+ config.AddConsumer>();
+
+ config.UsingInMemory((context, cfg) =>
+ {
+ cfg.ReceiveEndpoint("notification-queue", e =>
+ {
+ e.ConfigureConsumer>(context);
+ e.ConfigureConsumer>(context);
+ e.ConfigureConsumer>(context);
+ e.ConcurrentMessageLimit = 1;
+ e.PrefetchCount = 1;
+ });
+ });
+ });
private static IServiceCollection AddHttpClients(this IServiceCollection services, IConfiguration configuration)
{
diff --git a/code/Executable/DependencyInjection/NotificationsDI.cs b/code/Executable/DependencyInjection/NotificationsDI.cs
new file mode 100644
index 00000000..87bf3852
--- /dev/null
+++ b/code/Executable/DependencyInjection/NotificationsDI.cs
@@ -0,0 +1,16 @@
+using Infrastructure.Verticals.Notifications;
+using Infrastructure.Verticals.Notifications.Notifiarr;
+
+namespace Executable.DependencyInjection;
+
+public static class NotificationsDI
+{
+ public static IServiceCollection AddNotifications(this IServiceCollection services, IConfiguration configuration) =>
+ services
+ .Configure(configuration.GetSection(NotifiarrConfig.SectionName))
+ .AddTransient()
+ .AddTransient()
+ .AddTransient()
+ .AddTransient()
+ .AddTransient();
+}
\ No newline at end of file
diff --git a/code/Executable/appsettings.Development.json b/code/Executable/appsettings.Development.json
index 6f1eceb7..65f88e6f 100644
--- a/code/Executable/appsettings.Development.json
+++ b/code/Executable/appsettings.Development.json
@@ -86,5 +86,12 @@
"ApiKey": "7f677cfdc074414397af53dd633860c5"
}
]
+ },
+ "Notifiarr": {
+ "ON_IMPORT_FAILED_STRIKE": true,
+ "ON_STALLED_STRIKE": true,
+ "ON_QUEUE_ITEM_DELETE": true,
+ "API_KEY": "",
+ "CHANNEL_ID": ""
}
}
diff --git a/code/Executable/appsettings.json b/code/Executable/appsettings.json
index cd83efdd..18bfe546 100644
--- a/code/Executable/appsettings.json
+++ b/code/Executable/appsettings.json
@@ -83,5 +83,12 @@
"ApiKey": ""
}
]
+ },
+ "Notifiarr": {
+ "ON_IMPORT_FAILED_STRIKE": false,
+ "ON_STALLED_STRIKE": false,
+ "ON_QUEUE_ITEM_DELETE": false,
+ "API_KEY": "",
+ "CHANNEL_ID": ""
}
}
diff --git a/code/Infrastructure/Infrastructure.csproj b/code/Infrastructure/Infrastructure.csproj
index e6f34500..ed985e29 100644
--- a/code/Infrastructure/Infrastructure.csproj
+++ b/code/Infrastructure/Infrastructure.csproj
@@ -14,6 +14,8 @@
+
+
diff --git a/code/Infrastructure/Verticals/Arr/ArrClient.cs b/code/Infrastructure/Verticals/Arr/ArrClient.cs
index 65f400dd..7baa2fe5 100644
--- a/code/Infrastructure/Verticals/Arr/ArrClient.cs
+++ b/code/Infrastructure/Verticals/Arr/ArrClient.cs
@@ -66,7 +66,7 @@ public abstract class ArrClient
return queueResponse;
}
- public virtual bool ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload)
+ public virtual async Task ShouldRemoveFromQueue(InstanceType instanceType, QueueRecord record, bool isPrivateDownload)
{
if (_queueCleanerConfig.ImportFailedIgnorePrivate && isPrivateDownload)
{
@@ -96,7 +96,7 @@ public abstract class ArrClient
return false;
}
- return _striker.StrikeAndCheckLimit(
+ return await _striker.StrikeAndCheckLimit(
record.DownloadId,
record.Title,
_queueCleanerConfig.ImportFailedMaxStrikes,
diff --git a/code/Infrastructure/Verticals/Arr/SonarrClient.cs b/code/Infrastructure/Verticals/Arr/SonarrClient.cs
index ddd0295b..20ad6513 100644
--- a/code/Infrastructure/Verticals/Arr/SonarrClient.cs
+++ b/code/Infrastructure/Verticals/Arr/SonarrClient.cs
@@ -9,6 +9,7 @@ using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
+using Series = Domain.Models.Sonarr.Series;
namespace Infrastructure.Verticals.Arr;
@@ -26,7 +27,7 @@ public sealed class SonarrClient : ArrClient
protected override string GetQueueUrlPath(int page)
{
- return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true";
+ return $"/api/v3/queue?page={page}&pageSize=200&includeUnknownSeriesItems=true&includeSeries=true&includeEpisode=true";
}
protected override string GetQueueDeleteUrlPath(long recordId, bool removeFromClient)
diff --git a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
index f4a9f0f0..d70f9062 100644
--- a/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
+++ b/code/Infrastructure/Verticals/ContentBlocker/ContentBlocker.cs
@@ -7,8 +7,10 @@ using Domain.Enums;
using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
+using Infrastructure.Verticals.Context;
using Infrastructure.Verticals.DownloadClient;
using Infrastructure.Verticals.Jobs;
+using Infrastructure.Verticals.Notifications;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog.Context;
@@ -32,12 +34,15 @@ public sealed class ContentBlocker : GenericHandler
LidarrClient lidarrClient,
ArrQueueIterator arrArrQueueIterator,
BlocklistProvider blocklistProvider,
- DownloadServiceFactory downloadServiceFactory
+ DownloadServiceFactory downloadServiceFactory,
+ NotificationPublisher notifier
+
) : base(
logger, downloadClientConfig,
sonarrConfig, radarrConfig, lidarrConfig,
sonarrClient, radarrClient, lidarrClient,
- arrArrQueueIterator, downloadServiceFactory
+ arrArrQueueIterator, downloadServiceFactory,
+ notifier
)
{
_config = config.Value;
@@ -75,6 +80,10 @@ public sealed class ContentBlocker : GenericHandler
BlocklistType blocklistType = _blocklistProvider.GetBlocklistType(instanceType);
ConcurrentBag patterns = _blocklistProvider.GetPatterns(instanceType);
ConcurrentBag regexes = _blocklistProvider.GetRegexes(instanceType);
+
+ // push to context
+ ContextProvider.Set(nameof(ArrInstance) + nameof(ArrInstance.Url), instance.Url);
+ ContextProvider.Set(nameof(InstanceType), instanceType);
await _arrArrQueueIterator.Iterate(arrClient, instance, async items =>
{
@@ -97,6 +106,9 @@ public sealed class ContentBlocker : GenericHandler
continue;
}
+ // push record to context
+ ContextProvider.Set(nameof(QueueRecord), record);
+
_logger.LogDebug("searching unwanted files for {title}", record.Title);
BlockFilesResult result = await _downloadService
@@ -119,6 +131,7 @@ public sealed class ContentBlocker : GenericHandler
}
await arrClient.DeleteQueueItemAsync(instance, record, removeFromClient);
+ await _notifier.NotifyQueueItemDelete(removeFromClient, DeleteReason.AllFilesBlocked);
}
});
diff --git a/code/Infrastructure/Verticals/Context/ContextProvider.cs b/code/Infrastructure/Verticals/Context/ContextProvider.cs
new file mode 100644
index 00000000..071b91e0
--- /dev/null
+++ b/code/Infrastructure/Verticals/Context/ContextProvider.cs
@@ -0,0 +1,24 @@
+using System.Collections.Immutable;
+
+namespace Infrastructure.Verticals.Context;
+
+public static class ContextProvider
+{
+ private static readonly AsyncLocal> _asyncLocalDict = new();
+
+ public static void Set(string key, object value)
+ {
+ ImmutableDictionary currentDict = _asyncLocalDict.Value ?? ImmutableDictionary.Empty;
+ _asyncLocalDict.Value = currentDict.SetItem(key, value);
+ }
+
+ public static object? Get(string key)
+ {
+ return _asyncLocalDict.Value?.TryGetValue(key, out object? value) is true ? value : null;
+ }
+
+ public static T? Get(string key) where T : class
+ {
+ return Get(key) as T;
+ }
+}
diff --git a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs
index b64e467a..067b2b70 100644
--- a/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/Deluge/DelugeService.cs
@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner;
+using Domain.Enums;
using Domain.Models.Deluge.Response;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
@@ -71,8 +72,18 @@ public sealed class DelugeService : DownloadServiceBase
}
});
- result.ShouldRemove = shouldRemove || IsItemStuckAndShouldRemove(status);
+ if (shouldRemove)
+ {
+ result.DeleteReason = DeleteReason.AllFilesBlocked;
+ }
+
+ result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(status);
result.IsPrivate = status.Private;
+
+ if (!shouldRemove && result.ShouldRemove)
+ {
+ result.DeleteReason = DeleteReason.Stalled;
+ }
return result;
}
@@ -180,7 +191,7 @@ public sealed class DelugeService : DownloadServiceBase
await _client.DeleteTorrent(hash);
}
- private bool IsItemStuckAndShouldRemove(TorrentStatus status)
+ private async Task IsItemStuckAndShouldRemove(TorrentStatus status)
{
if (_queueCleanerConfig.StalledMaxStrikes is 0)
{
@@ -206,7 +217,7 @@ public sealed class DelugeService : DownloadServiceBase
ResetStrikesOnProgress(status.Hash!, status.TotalDone);
- return StrikeAndCheckLimit(status.Hash!, status.Name!);
+ return await StrikeAndCheckLimit(status.Hash!, status.Name!);
}
private async Task GetTorrentStatus(string hash)
diff --git a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs
index 91f37c2a..1d896cb3 100644
--- a/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/DownloadServiceBase.cs
@@ -83,8 +83,8 @@ public abstract class DownloadServiceBase : IDownloadService
/// The torrent hash.
/// The name or title of the item.
/// True if the limit has been reached; otherwise, false.
- protected bool StrikeAndCheckLimit(string hash, string itemName)
+ protected async Task StrikeAndCheckLimit(string hash, string itemName)
{
- return _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled);
+ return await _striker.StrikeAndCheckLimit(hash, itemName, _queueCleanerConfig.StalledMaxStrikes, StrikeType.Stalled);
}
}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs
index 19bf99e4..d082856f 100644
--- a/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/QBittorrent/QBitService.cs
@@ -4,6 +4,7 @@ using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner;
using Common.Helpers;
+using Domain.Enums;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Caching.Memory;
@@ -73,6 +74,7 @@ public sealed class QBitService : DownloadServiceBase
if (torrent is { CompletionOn: not null, Downloaded: null or 0 })
{
result.ShouldRemove = true;
+ result.DeleteReason = DeleteReason.AllFilesBlocked;
return result;
}
@@ -82,10 +84,16 @@ public sealed class QBitService : DownloadServiceBase
if (files?.Count is > 0 && files.All(x => x.Priority is TorrentContentPriority.Skip))
{
result.ShouldRemove = true;
+ result.DeleteReason = DeleteReason.AllFilesBlocked;
return result;
}
- result.ShouldRemove = IsItemStuckAndShouldRemove(torrent, result.IsPrivate);
+ result.ShouldRemove = await IsItemStuckAndShouldRemove(torrent, result.IsPrivate);
+
+ if (result.ShouldRemove)
+ {
+ result.DeleteReason = DeleteReason.Stalled;
+ }
return result;
}
@@ -197,7 +205,7 @@ public sealed class QBitService : DownloadServiceBase
_client.Dispose();
}
- private bool IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
+ private async Task IsItemStuckAndShouldRemove(TorrentInfo torrent, bool isPrivate)
{
if (_queueCleanerConfig.StalledMaxStrikes is 0)
{
@@ -220,6 +228,6 @@ public sealed class QBitService : DownloadServiceBase
ResetStrikesOnProgress(torrent.Hash, torrent.Downloaded ?? 0);
- return StrikeAndCheckLimit(torrent.Hash, torrent.Name);
+ return await StrikeAndCheckLimit(torrent.Hash, torrent.Name);
}
}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/DownloadClient/StalledResult.cs b/code/Infrastructure/Verticals/DownloadClient/StalledResult.cs
index ead7572b..a1f35f81 100644
--- a/code/Infrastructure/Verticals/DownloadClient/StalledResult.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/StalledResult.cs
@@ -1,4 +1,6 @@
-namespace Infrastructure.Verticals.DownloadClient;
+using Domain.Enums;
+
+namespace Infrastructure.Verticals.DownloadClient;
public sealed record StalledResult
{
@@ -7,6 +9,8 @@ public sealed record StalledResult
///
public bool ShouldRemove { get; set; }
+ public DeleteReason DeleteReason { get; set; }
+
///
/// True if the download is private; otherwise false.
///
diff --git a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs
index b53a0ba1..92c88a7a 100644
--- a/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs
+++ b/code/Infrastructure/Verticals/DownloadClient/Transmission/TransmissionService.cs
@@ -4,6 +4,7 @@ using Common.Configuration.ContentBlocker;
using Common.Configuration.DownloadClient;
using Common.Configuration.QueueCleaner;
using Common.Helpers;
+using Domain.Enums;
using Infrastructure.Verticals.ContentBlocker;
using Infrastructure.Verticals.ItemStriker;
using Microsoft.Extensions.Caching.Memory;
@@ -76,9 +77,19 @@ public sealed class TransmissionService : DownloadServiceBase
shouldRemove = false;
}
}
+
+ if (shouldRemove)
+ {
+ result.DeleteReason = DeleteReason.AllFilesBlocked;
+ }
// remove if all files are unwanted or download is stuck
- result.ShouldRemove = shouldRemove || IsItemStuckAndShouldRemove(torrent);
+ result.ShouldRemove = shouldRemove || await IsItemStuckAndShouldRemove(torrent);
+
+ if (!shouldRemove && result.ShouldRemove)
+ {
+ result.DeleteReason = DeleteReason.Stalled;
+ }
return result;
}
@@ -178,7 +189,7 @@ public sealed class TransmissionService : DownloadServiceBase
{
}
- private bool IsItemStuckAndShouldRemove(TorrentInfo torrent)
+ private async Task IsItemStuckAndShouldRemove(TorrentInfo torrent)
{
if (_queueCleanerConfig.StalledMaxStrikes is 0)
{
@@ -205,7 +216,7 @@ public sealed class TransmissionService : DownloadServiceBase
ResetStrikesOnProgress(torrent.HashString!, torrent.DownloadedEver ?? 0);
- return StrikeAndCheckLimit(torrent.HashString!, torrent.Name!);
+ return await StrikeAndCheckLimit(torrent.HashString!, torrent.Name!);
}
private async Task GetTorrentAsync(string hash)
diff --git a/code/Infrastructure/Verticals/ItemStriker/Striker.cs b/code/Infrastructure/Verticals/ItemStriker/Striker.cs
index dcd7308b..c1ce8ec7 100644
--- a/code/Infrastructure/Verticals/ItemStriker/Striker.cs
+++ b/code/Infrastructure/Verticals/ItemStriker/Striker.cs
@@ -1,6 +1,8 @@
using Common.Helpers;
using Domain.Enums;
using Infrastructure.Helpers;
+using Infrastructure.Verticals.Context;
+using Infrastructure.Verticals.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
@@ -11,16 +13,18 @@ public class Striker
private readonly ILogger _logger;
private readonly IMemoryCache _cache;
private readonly MemoryCacheEntryOptions _cacheOptions;
+ private readonly NotificationPublisher _notifier;
- public Striker(ILogger logger, IMemoryCache cache)
+ public Striker(ILogger logger, IMemoryCache cache, NotificationPublisher notifier)
{
_logger = logger;
_cache = cache;
+ _notifier = notifier;
_cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(StaticConfiguration.TriggerValue + Constants.CacheLimitBuffer);
}
- public bool StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType)
+ public async Task StrikeAndCheckLimit(string hash, string itemName, ushort maxStrikes, StrikeType strikeType)
{
if (maxStrikes is 0)
{
@@ -29,7 +33,7 @@ public class Striker
string key = CacheKeys.Strike(strikeType, hash);
- if (!_cache.TryGetValue(key, out int? strikeCount))
+ if (!_cache.TryGetValue(key, out int strikeCount))
{
strikeCount = 1;
}
@@ -39,6 +43,9 @@ public class Striker
}
_logger.LogInformation("item on strike number {strike} | reason {reason} | {name}", strikeCount, strikeType.ToString(), itemName);
+
+ await _notifier.NotifyStrike(strikeType, strikeCount);
+
_cache.Set(key, strikeCount, _cacheOptions);
if (strikeCount < maxStrikes)
diff --git a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs
index 83e4dc54..dc40f0ee 100644
--- a/code/Infrastructure/Verticals/Jobs/GenericHandler.cs
+++ b/code/Infrastructure/Verticals/Jobs/GenericHandler.cs
@@ -5,6 +5,7 @@ using Domain.Models.Arr;
using Domain.Models.Arr.Queue;
using Infrastructure.Verticals.Arr;
using Infrastructure.Verticals.DownloadClient;
+using Infrastructure.Verticals.Notifications;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -22,6 +23,7 @@ public abstract class GenericHandler : IDisposable
protected readonly LidarrClient _lidarrClient;
protected readonly ArrQueueIterator _arrArrQueueIterator;
protected readonly IDownloadService _downloadService;
+ protected readonly NotificationPublisher _notifier;
protected GenericHandler(
ILogger logger,
@@ -33,7 +35,8 @@ public abstract class GenericHandler : IDisposable
RadarrClient radarrClient,
LidarrClient lidarrClient,
ArrQueueIterator arrArrQueueIterator,
- DownloadServiceFactory downloadServiceFactory
+ DownloadServiceFactory downloadServiceFactory,
+ NotificationPublisher notifier
)
{
_logger = logger;
@@ -46,6 +49,7 @@ public abstract class GenericHandler : IDisposable
_lidarrClient = lidarrClient;
_arrArrQueueIterator = arrArrQueueIterator;
_downloadService = downloadServiceFactory.CreateDownloadClient();
+ _notifier = notifier;
}
public virtual async Task ExecuteAsync()
diff --git a/code/Infrastructure/Verticals/Notifications/Consumers/NotificationConsumer.cs b/code/Infrastructure/Verticals/Notifications/Consumers/NotificationConsumer.cs
new file mode 100644
index 00000000..166155ee
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Consumers/NotificationConsumer.cs
@@ -0,0 +1,45 @@
+using Infrastructure.Verticals.Notifications.Models;
+using MassTransit;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Verticals.Notifications.Consumers;
+
+public sealed class NotificationConsumer : IConsumer where T : Notification
+{
+ private readonly ILogger> _logger;
+ private readonly NotificationService _notificationService;
+
+ public NotificationConsumer(ILogger> logger, NotificationService notificationService)
+ {
+ _logger = logger;
+ _notificationService = notificationService;
+ }
+
+ public async Task Consume(ConsumeContext context)
+ {
+ try
+ {
+ switch (context.Message)
+ {
+ case FailedImportStrikeNotification failedMessage:
+ await _notificationService.Notify(failedMessage);
+ break;
+ case StalledStrikeNotification stalledMessage:
+ await _notificationService.Notify(stalledMessage);
+ break;
+ case QueueItemDeleteNotification queueItemDeleteMessage:
+ await _notificationService.Notify(queueItemDeleteMessage);
+ break;
+ default:
+ throw new NotImplementedException();
+ }
+
+ // prevent spamming
+ await Task.Delay(1000);
+ }
+ catch (Exception exception)
+ {
+ _logger.LogError(exception, "error while processing notifications");
+ }
+ }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/INotificationFactory.cs b/code/Infrastructure/Verticals/Notifications/INotificationFactory.cs
new file mode 100644
index 00000000..bfb26355
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/INotificationFactory.cs
@@ -0,0 +1,10 @@
+namespace Infrastructure.Verticals.Notifications;
+
+public interface INotificationFactory
+{
+ List OnFailedImportStrikeEnabled();
+
+ List OnStalledStrikeEnabled();
+
+ List OnQueueItemDeleteEnabled();
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/INotificationProvider.cs b/code/Infrastructure/Verticals/Notifications/INotificationProvider.cs
new file mode 100644
index 00000000..1dc00b64
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/INotificationProvider.cs
@@ -0,0 +1,17 @@
+using Common.Configuration.Notification;
+using Infrastructure.Verticals.Notifications.Models;
+
+namespace Infrastructure.Verticals.Notifications;
+
+public interface INotificationProvider
+{
+ NotificationConfig Config { get; }
+
+ string Name { get; }
+
+ Task OnFailedImportStrike(FailedImportStrikeNotification notification);
+
+ Task OnStalledStrike(StalledStrikeNotification notification);
+
+ Task OnQueueItemDelete(QueueItemDeleteNotification notification);
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Models/FailedImportStrikeNotification.cs b/code/Infrastructure/Verticals/Notifications/Models/FailedImportStrikeNotification.cs
new file mode 100644
index 00000000..b8ac5fa5
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Models/FailedImportStrikeNotification.cs
@@ -0,0 +1,5 @@
+namespace Infrastructure.Verticals.Notifications.Models;
+
+public sealed record FailedImportStrikeNotification : Notification
+{
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Models/Notification.cs b/code/Infrastructure/Verticals/Notifications/Models/Notification.cs
new file mode 100644
index 00000000..910a6107
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Models/Notification.cs
@@ -0,0 +1,20 @@
+using Domain.Enums;
+
+namespace Infrastructure.Verticals.Notifications.Models;
+
+public record Notification
+{
+ public required InstanceType InstanceType { get; init; }
+
+ public required Uri InstanceUrl { get; init; }
+
+ public required string Hash { get; init; }
+
+ public required string Title { get; init; }
+
+ public required string Description { get; init; }
+
+ public Uri? Image { get; init; }
+
+ public List? Fields { get; init; }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Models/NotificationField.cs b/code/Infrastructure/Verticals/Notifications/Models/NotificationField.cs
new file mode 100644
index 00000000..1d8621f1
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Models/NotificationField.cs
@@ -0,0 +1,8 @@
+namespace Infrastructure.Verticals.Notifications.Models;
+
+public sealed record NotificationField
+{
+ public required string Title { get; init; }
+
+ public required string Text { get; init; }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Models/QueueItemDeleteNotification.cs b/code/Infrastructure/Verticals/Notifications/Models/QueueItemDeleteNotification.cs
new file mode 100644
index 00000000..0a5b2cab
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Models/QueueItemDeleteNotification.cs
@@ -0,0 +1,5 @@
+namespace Infrastructure.Verticals.Notifications.Models;
+
+public sealed record QueueItemDeleteNotification : Notification
+{
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Models/StalledStrikeNotification.cs b/code/Infrastructure/Verticals/Notifications/Models/StalledStrikeNotification.cs
new file mode 100644
index 00000000..74f17ba8
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Models/StalledStrikeNotification.cs
@@ -0,0 +1,5 @@
+namespace Infrastructure.Verticals.Notifications.Models;
+
+public sealed record StalledStrikeNotification : Notification
+{
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/INotifiarrProxy.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/INotifiarrProxy.cs
new file mode 100644
index 00000000..d54c8faa
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/INotifiarrProxy.cs
@@ -0,0 +1,6 @@
+namespace Infrastructure.Verticals.Notifications.Notifiarr;
+
+public interface INotifiarrProxy
+{
+ Task SendNotification(NotifiarrPayload payload, NotifiarrConfig config);
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrConfig.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrConfig.cs
new file mode 100644
index 00000000..489e62a3
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrConfig.cs
@@ -0,0 +1,30 @@
+using Common.Configuration.Notification;
+using Microsoft.Extensions.Configuration;
+
+namespace Infrastructure.Verticals.Notifications.Notifiarr;
+
+public sealed record NotifiarrConfig : NotificationConfig
+{
+ public const string SectionName = "Notifiarr";
+
+ [ConfigurationKeyName("API_KEY")]
+ public string? ApiKey { get; init; }
+
+ [ConfigurationKeyName("CHANNEL_ID")]
+ public string? ChannelId { get; init; }
+
+ public override bool IsValid()
+ {
+ if (string.IsNullOrEmpty(ApiKey?.Trim()))
+ {
+ return false;
+ }
+
+ if (string.IsNullOrEmpty(ChannelId?.Trim()))
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrException.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrException.cs
new file mode 100644
index 00000000..10ca5908
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrException.cs
@@ -0,0 +1,12 @@
+namespace Infrastructure.Verticals.Notifications.Notifiarr;
+
+public class NotifiarrException : Exception
+{
+ public NotifiarrException(string message) : base(message)
+ {
+ }
+
+ public NotifiarrException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrPayload.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrPayload.cs
new file mode 100644
index 00000000..24e5da9b
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrPayload.cs
@@ -0,0 +1,57 @@
+namespace Infrastructure.Verticals.Notifications.Notifiarr;
+
+public class NotifiarrPayload
+{
+ public NotifiarrNotification Notification { get; set; } = new NotifiarrNotification();
+ public Discord Discord { get; set; }
+}
+
+public class NotifiarrNotification
+{
+ public bool Update { get; set; }
+ public string Name => "Cleanuperr";
+ public int? Event { get; set; }
+}
+
+public class Discord
+{
+ public string Color { get; set; } = string.Empty;
+ public Ping Ping { get; set; }
+ public Images Images { get; set; }
+ public Text Text { get; set; }
+ public Ids Ids { get; set; }
+}
+
+public class Ping
+{
+ public string PingUser { get; set; }
+ public string PingRole { get; set; }
+}
+
+public class Images
+{
+ public Uri? Thumbnail { get; set; }
+ public Uri? Image { get; set; }
+}
+
+public class Text
+{
+ public string Title { get; set; } = string.Empty;
+ public string Icon { get; set; } = string.Empty;
+ public string Content { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ public List Fields { get; set; } = new List();
+ public string Footer { get; set; } = string.Empty;
+}
+
+public class Field
+{
+ public string Title { get; set; } = string.Empty;
+ public string Text { get; set; } = string.Empty;
+ public bool Inline { get; set; }
+}
+
+public class Ids
+{
+ public string Channel { get; set; }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProvider.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProvider.cs
new file mode 100644
index 00000000..dd189fb9
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProvider.cs
@@ -0,0 +1,75 @@
+using Domain.Enums;
+using Infrastructure.Verticals.Notifications.Models;
+using Mapster;
+using Microsoft.Extensions.Options;
+
+namespace Infrastructure.Verticals.Notifications.Notifiarr;
+
+public class NotifiarrProvider : NotificationProvider
+{
+ private readonly NotifiarrConfig _config;
+ private readonly INotifiarrProxy _proxy;
+
+ private const string WarningColor = "f0ad4e";
+ private const string ImportantColor = "bb2124";
+
+ public NotifiarrProvider(IOptions config, INotifiarrProxy proxy)
+ : base(config)
+ {
+ _config = config.Value;
+ _proxy = proxy;
+ }
+
+ public override string Name => "Notifiarr";
+
+ public override async Task OnFailedImportStrike(FailedImportStrikeNotification notification)
+ {
+ await _proxy.SendNotification(BuildPayload(notification, WarningColor), _config);
+ }
+
+ public override async Task OnStalledStrike(StalledStrikeNotification notification)
+ {
+ await _proxy.SendNotification(BuildPayload(notification, WarningColor), _config);
+ }
+
+ public override async Task OnQueueItemDelete(QueueItemDeleteNotification notification)
+ {
+ await _proxy.SendNotification(BuildPayload(notification, ImportantColor), _config);
+ }
+
+ private NotifiarrPayload BuildPayload(Notification notification, string color)
+ {
+ NotifiarrPayload payload = new()
+ {
+ Discord = new()
+ {
+ Color = color,
+ Text = new()
+ {
+ Title = notification.Title,
+ Icon = "https://github.com/flmorg/cleanuperr/blob/main/Logo/48.png?raw=true",
+ Description = notification.Description,
+ Fields = new()
+ {
+ new() { Title = "Instance type", Text = notification.InstanceType.ToString() },
+ new() { Title = "Url", Text = notification.InstanceUrl.ToString() },
+ new() { Title = "Download hash", Text = notification.Hash }
+ }
+ },
+ Ids = new Ids
+ {
+ Channel = _config.ChannelId
+ },
+ Images = new()
+ {
+ Thumbnail = new Uri("https://github.com/flmorg/cleanuperr/blob/main/Logo/48.png?raw=true"),
+ Image = notification.Image
+ }
+ }
+ };
+
+ payload.Discord.Text.Fields.AddRange(notification.Fields?.Adapt>() ?? []);
+
+ return payload;
+ }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProxy.cs b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProxy.cs
new file mode 100644
index 00000000..edfc01a1
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/Notifiarr/NotifiarrProxy.cs
@@ -0,0 +1,55 @@
+using System.Text;
+using Common.Helpers;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace Infrastructure.Verticals.Notifications.Notifiarr;
+
+public class NotifiarrProxy : INotifiarrProxy
+{
+ private readonly HttpClient _httpClient;
+
+ private const string Url = "https://notifiarr.com/api/v1/notification/passthrough/";
+
+ public NotifiarrProxy(IHttpClientFactory httpClientFactory)
+ {
+ _httpClient = httpClientFactory.CreateClient(Constants.HttpClientWithRetryName);
+ }
+
+ public async Task SendNotification(NotifiarrPayload payload, NotifiarrConfig config)
+ {
+ try
+ {
+ string content = JsonConvert.SerializeObject(payload, new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver()
+ });
+
+ using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"{Url}{config.ApiKey}");
+ request.Method = HttpMethod.Post;
+ request.Content = new StringContent(content, Encoding.UTF8, "application/json");
+
+ using HttpResponseMessage response = await _httpClient.SendAsync(request);
+ response.EnsureSuccessStatusCode();
+ }
+ catch (HttpRequestException exception)
+ {
+ if (exception.StatusCode is null)
+ {
+ throw new NotifiarrException("unable to send notification", exception);
+ }
+
+ switch ((int)exception.StatusCode)
+ {
+ case 401:
+ throw new NotifiarrException("unable to send notification | API key is invalid");
+ case 502:
+ case 503:
+ case 504:
+ throw new NotifiarrException("unable to send notification | service unavailable", exception);
+ default:
+ throw new NotifiarrException("unable to send notification", exception);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/NotificationFactory.cs b/code/Infrastructure/Verticals/Notifications/NotificationFactory.cs
new file mode 100644
index 00000000..bc942527
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/NotificationFactory.cs
@@ -0,0 +1,32 @@
+namespace Infrastructure.Verticals.Notifications;
+
+public class NotificationFactory : INotificationFactory
+{
+ private readonly IEnumerable _providers;
+
+ public NotificationFactory(IEnumerable providers)
+ {
+ _providers = providers;
+ }
+
+ protected List ActiveProviders() =>
+ _providers
+ .Where(x => x.Config.IsValid())
+ .Where(provider => provider.Config.IsEnabled)
+ .ToList();
+
+ public List OnFailedImportStrikeEnabled() =>
+ ActiveProviders()
+ .Where(n => n.Config.OnImportFailedStrike)
+ .ToList();
+
+ public List OnStalledStrikeEnabled() =>
+ ActiveProviders()
+ .Where(n => n.Config.OnStalledStrike)
+ .ToList();
+
+ public List OnQueueItemDeleteEnabled() =>
+ ActiveProviders()
+ .Where(n => n.Config.OnQueueItemDelete)
+ .ToList();
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/NotificationProvider.cs b/code/Infrastructure/Verticals/Notifications/NotificationProvider.cs
new file mode 100644
index 00000000..749ba4d2
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/NotificationProvider.cs
@@ -0,0 +1,23 @@
+using Common.Configuration.Notification;
+using Infrastructure.Verticals.Notifications.Models;
+using Microsoft.Extensions.Options;
+
+namespace Infrastructure.Verticals.Notifications;
+
+public abstract class NotificationProvider : INotificationProvider
+{
+ protected NotificationProvider(IOptions config)
+ {
+ Config = config.Value;
+ }
+
+ public abstract string Name { get; }
+
+ public NotificationConfig Config { get; }
+
+ public abstract Task OnFailedImportStrike(FailedImportStrikeNotification notification);
+
+ public abstract Task OnStalledStrike(StalledStrikeNotification notification);
+
+ public abstract Task OnQueueItemDelete(QueueItemDeleteNotification notification);
+}
\ No newline at end of file
diff --git a/code/Infrastructure/Verticals/Notifications/NotificationPublisher.cs b/code/Infrastructure/Verticals/Notifications/NotificationPublisher.cs
new file mode 100644
index 00000000..b8b486b6
--- /dev/null
+++ b/code/Infrastructure/Verticals/Notifications/NotificationPublisher.cs
@@ -0,0 +1,99 @@
+using Common.Configuration.Arr;
+using Domain.Enums;
+using Domain.Models.Arr.Queue;
+using Infrastructure.Verticals.Context;
+using Infrastructure.Verticals.Notifications.Models;
+using Mapster;
+using MassTransit;
+using Microsoft.Extensions.Logging;
+
+namespace Infrastructure.Verticals.Notifications;
+
+public sealed class NotificationPublisher
+{
+ private readonly ILogger _logger;
+ private readonly IBus _messageBus;
+
+ public NotificationPublisher(ILogger logger, IBus messageBus)
+ {
+ _logger = logger;
+ _messageBus = messageBus;
+ }
+
+ public async Task NotifyStrike(StrikeType strikeType, int strikeCount)
+ {
+ try
+ {
+ QueueRecord record = GetRecordFromContext();
+ InstanceType instanceType = GetInstanceTypeFromContext();
+ Uri instanceUrl = GetInstanceUrlFromContext();
+ Uri? imageUrl = GetImageFromContext(record, instanceType);
+
+ Notification notification = new()
+ {
+ InstanceType = instanceType,
+ InstanceUrl = instanceUrl,
+ Hash = record.DownloadId.ToLowerInvariant(),
+ Title = $"Strike received with reason: {strikeType}",
+ Description = record.Title,
+ Image = imageUrl,
+ Fields = [new() { Title = "Strike count", Text = strikeCount.ToString() }]
+ };
+
+ switch (strikeType)
+ {
+ case StrikeType.Stalled:
+ await _messageBus.Publish(notification.Adapt());
+ break;
+ case StrikeType.ImportFailed:
+ await _messageBus.Publish(notification.Adapt());
+ break;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "failed to notify strike");
+ }
+ }
+
+ public async Task NotifyQueueItemDelete(bool removeFromClient, DeleteReason reason)
+ {
+ QueueRecord record = GetRecordFromContext();
+ InstanceType instanceType = GetInstanceTypeFromContext();
+ Uri instanceUrl = GetInstanceUrlFromContext();
+ Uri? imageUrl = GetImageFromContext(record, instanceType);
+
+ Notification notification = new()
+ {
+ InstanceType = instanceType,
+ InstanceUrl = instanceUrl,
+ Hash = record.DownloadId.ToLowerInvariant(),
+ Title = $"Deleting item from queue with reason: {reason}",
+ Description = record.Title,
+ Image = imageUrl,
+ Fields = [new() { Title = "Removed from download client?", Text = removeFromClient ? "Yes" : "No" }]
+ };
+
+ await _messageBus.Publish(notification.Adapt());
+ }
+
+ private static QueueRecord GetRecordFromContext() =>
+ ContextProvider.Get(nameof(QueueRecord)) ?? throw new Exception("failed to get record from context");
+
+ private static InstanceType GetInstanceTypeFromContext() =>
+ (InstanceType)(ContextProvider.Get