mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 10:48:49 -04:00
added seeker cycle completed notification event
This commit is contained in:
1 parent
c5e6865144
commit
12fa674246
22 files changed
+4547
-6
No files matched your search
+7
-3
@@ -152,7 +152,8 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
OnSearchTriggered = request.OnSearchTriggered,
|
||||
OnSearchItemGrabbed = request.OnSearchItemGrabbed,
|
||||
OnForceImported = request.OnForceImported,
|
||||
OnErroredStrike = request.OnErroredStrike
|
||||
OnErroredStrike = request.OnErroredStrike,
|
||||
OnSeekerCycleCompleted = request.OnSeekerCycleCompleted
|
||||
}, config);
|
||||
|
||||
await _dataContext.NotificationConfigs.AddAsync(provider);
|
||||
@@ -229,6 +230,7 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
OnSearchItemGrabbed = request.OnSearchItemGrabbed,
|
||||
OnForceImported = request.OnForceImported,
|
||||
OnErroredStrike = request.OnErroredStrike,
|
||||
OnSeekerCycleCompleted = request.OnSeekerCycleCompleted,
|
||||
UpdatedAt = _timeProvider.GetUtcNow()
|
||||
}, newConfig);
|
||||
|
||||
@@ -331,7 +333,8 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
OnSearchTriggered = false,
|
||||
OnSearchItemGrabbed = false,
|
||||
OnForceImported = false,
|
||||
OnErroredStrike = false
|
||||
OnErroredStrike = false,
|
||||
OnSeekerCycleCompleted = false
|
||||
},
|
||||
Configuration = config
|
||||
};
|
||||
@@ -367,7 +370,8 @@ public sealed class NotificationProvidersController : ControllerBase
|
||||
OnSearchTriggered = provider.OnSearchTriggered,
|
||||
OnSearchItemGrabbed = provider.OnSearchItemGrabbed,
|
||||
OnForceImported = provider.OnForceImported,
|
||||
OnErroredStrike = provider.OnErroredStrike
|
||||
OnErroredStrike = provider.OnErroredStrike,
|
||||
OnSeekerCycleCompleted = provider.OnSeekerCycleCompleted
|
||||
},
|
||||
Configuration = (object?)descriptor.GetConfig(provider) ?? new object()
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ public enum EventType
|
||||
ForceImported,
|
||||
DownloadStopped,
|
||||
ErroredStrike,
|
||||
SeekerCycleCompleted,
|
||||
|
||||
/// <summary>
|
||||
/// Text this build does not know.
|
||||
|
||||
@@ -14,5 +14,6 @@ public enum NotificationEventType
|
||||
SearchItemGrabbed,
|
||||
DownloadStopped,
|
||||
ForceImported,
|
||||
ErroredStrike
|
||||
ErroredStrike,
|
||||
SeekerCycleCompleted
|
||||
}
|
||||
@@ -1115,4 +1115,102 @@ public class EventPublisherTests : IDisposable
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PublishSeekerCycleCompleted Tests
|
||||
|
||||
private async Task SeedSearchTriggered(Guid cycleId, SeekerSearchReason reason, bool grabbed)
|
||||
{
|
||||
_context.Events.Add(new AppEvent
|
||||
{
|
||||
EventType = EventType.SearchTriggered,
|
||||
Message = "Search triggered",
|
||||
Severity = EventSeverity.Information,
|
||||
CycleId = cycleId,
|
||||
SearchReason = reason,
|
||||
GrabbedItems = grabbed ? ["Release A"] : []
|
||||
});
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static SeekerCycle CreateCompletedCycle(bool isDryRun = false)
|
||||
{
|
||||
DateTimeOffset startedAt = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
return new SeekerCycle
|
||||
{
|
||||
StartedAt = startedAt,
|
||||
CompletedAt = startedAt.AddHours(5).AddMinutes(30),
|
||||
SearchedUnits = 120,
|
||||
GrabbedUnits = 14,
|
||||
IsDryRun = isDryRun
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishSeekerCycleCompleted_SavesEventAndNotifies()
|
||||
{
|
||||
// Arrange
|
||||
SeekerCycle cycle = CreateCompletedCycle();
|
||||
|
||||
// Act
|
||||
await _publisher.PublishSeekerCycleCompleted(cycle, "Radarr Main");
|
||||
|
||||
// Assert
|
||||
AppEvent? savedEvent = await _context.Events.SingleOrDefaultAsync(e => e.EventType == EventType.SeekerCycleCompleted);
|
||||
savedEvent.ShouldNotBeNull();
|
||||
savedEvent.CycleId.ShouldBe(cycle.Id);
|
||||
await _notificationPublisher.Received(1).NotifySeekerCycleCompleted(
|
||||
"Radarr Main", TimeSpan.FromHours(5.5), 120, 14, 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishSeekerCycleCompleted_CalledTwiceForSameCycle_PublishesOnce()
|
||||
{
|
||||
// Arrange
|
||||
SeekerCycle cycle = CreateCompletedCycle();
|
||||
|
||||
// Act
|
||||
await _publisher.PublishSeekerCycleCompleted(cycle, "Radarr Main");
|
||||
await _publisher.PublishSeekerCycleCompleted(cycle, "Radarr Main");
|
||||
|
||||
// Assert
|
||||
(await _context.Events.CountAsync(e => e.EventType == EventType.SeekerCycleCompleted)).ShouldBe(1);
|
||||
await _notificationPublisher.Received(1).NotifySeekerCycleCompleted(
|
||||
Arg.Any<string>(), Arg.Any<TimeSpan>(), Arg.Any<int>(), Arg.Any<int>(), Arg.Any<int>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishSeekerCycleCompleted_CountsOnlyGrabbedUpgradeSearchesOfThisCycle()
|
||||
{
|
||||
// Arrange
|
||||
SeekerCycle cycle = CreateCompletedCycle();
|
||||
await SeedSearchTriggered(cycle.Id, SeekerSearchReason.QualityCutoffNotMet, grabbed: true);
|
||||
await SeedSearchTriggered(cycle.Id, SeekerSearchReason.CustomFormatScoreBelowCutoff, grabbed: true);
|
||||
await SeedSearchTriggered(cycle.Id, SeekerSearchReason.Missing, grabbed: true);
|
||||
await SeedSearchTriggered(cycle.Id, SeekerSearchReason.QualityCutoffNotMet, grabbed: false);
|
||||
await SeedSearchTriggered(Guid.NewGuid(), SeekerSearchReason.QualityCutoffNotMet, grabbed: true);
|
||||
|
||||
// Act
|
||||
await _publisher.PublishSeekerCycleCompleted(cycle, "Radarr Main");
|
||||
|
||||
// Assert
|
||||
await _notificationPublisher.Received(1).NotifySeekerCycleCompleted(
|
||||
"Radarr Main", Arg.Any<TimeSpan>(), 120, 14, 2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishSeekerCycleCompleted_ForDryRunCycle_SavesDryRunEvent()
|
||||
{
|
||||
// Arrange
|
||||
SeekerCycle cycle = CreateCompletedCycle(isDryRun: true);
|
||||
|
||||
// Act
|
||||
await _publisher.PublishSeekerCycleCompleted(cycle, "Radarr Main");
|
||||
|
||||
// Assert
|
||||
AppEvent? savedEvent = await _context.Events.SingleOrDefaultAsync(e => e.EventType == EventType.SeekerCycleCompleted);
|
||||
savedEvent.ShouldNotBeNull();
|
||||
savedEvent.IsDryRun.ShouldBeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -7963,6 +7963,8 @@ public class SeekerTests : IDisposable
|
||||
|
||||
// Assert: the empty cycle stays open
|
||||
await mockArrClient.DidNotReceive().SearchItemAsync(radarrInstance, Arg.Any<SearchItem>());
|
||||
await _fixture.EventPublisher.DidNotReceive()
|
||||
.PublishSeekerCycleCompleted(Arg.Any<SeekerCycle>(), Arg.Any<string>());
|
||||
|
||||
var cycle = await _fixture.EventsContext.SeekerCycles.FirstAsync(c => c.Id == currentCycleId);
|
||||
cycle.CompletedAt.ShouldBeNull();
|
||||
@@ -8455,6 +8457,77 @@ public class SeekerTests : IDisposable
|
||||
cycle.CompletionReason.ShouldBe(SeekerCycleCompletionReason.Completed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Radarr_CycleCompletes_PublishesCycleCompletedOnlyOnce()
|
||||
{
|
||||
// Arrange: the cycle searched 2 movies and the only remaining candidate is in the queue
|
||||
SeekerConfig config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
|
||||
ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
Guid currentCycleId = Guid.NewGuid();
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7
|
||||
});
|
||||
|
||||
_fixture.EventsContext.SeekerCycles.Add(new SeekerCycle
|
||||
{
|
||||
Id = currentCycleId,
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
StartedAt = _fixture.TimeProvider.GetUtcNow(),
|
||||
SearchedUnits = 2,
|
||||
IsDryRun = false
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
|
||||
QueueRecord[] queuedRecords =
|
||||
[
|
||||
new() { Id = 1, Title = "Movie 1 Download", DownloadId = "hash1", Protocol = "torrent", SizeLeft = 1000, MovieId = 1, TrackedDownloadState = "downloading" }
|
||||
];
|
||||
_fixture.ArrQueueReader
|
||||
.ReadAllAsync(mockArrClient, Arg.Any<ArrInstance>())
|
||||
.Returns(queuedRecords.ToList());
|
||||
|
||||
List<SearchableMovie> movies =
|
||||
[
|
||||
new SearchableMovie { Id = 1, Title = "Movie 1", Status = "released", Monitored = true, Tags = [] }
|
||||
];
|
||||
_radarrClient
|
||||
.StreamAllMoviesAsync(radarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(ToAsyncEnumerable(movies));
|
||||
StubRadarrWanted(radarrInstance, movies);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
// Act 1: the cycle completes
|
||||
await CreateSut().ExecuteAsync();
|
||||
|
||||
// Assert 1
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishSeekerCycleCompleted(Arg.Is<SeekerCycle>(c => c.Id == currentCycleId), Arg.Any<string>());
|
||||
|
||||
// Act 2: a later run while the instance waits for MinCycleTimeDays
|
||||
await CreateSut().ExecuteAsync();
|
||||
|
||||
// Assert 2
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishSeekerCycleCompleted(Arg.Any<SeekerCycle>(), Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Sonarr_AllSeasonsQueued_CompletesCycleWithPriorSearches()
|
||||
{
|
||||
|
||||
+3
@@ -180,6 +180,7 @@ public class NotificationConfigurationServiceTests : IDisposable
|
||||
[InlineData(NotificationEventType.SearchItemGrabbed)]
|
||||
[InlineData(NotificationEventType.ForceImported)]
|
||||
[InlineData(NotificationEventType.ErroredStrike)]
|
||||
[InlineData(NotificationEventType.SeekerCycleCompleted)]
|
||||
public async Task GetProvidersForEventAsync_ReturnsProviderForCorrectEvents(NotificationEventType eventType)
|
||||
{
|
||||
// Arrange
|
||||
@@ -194,6 +195,7 @@ public class NotificationConfigurationServiceTests : IDisposable
|
||||
bool onSearchItemGrabbed = eventType == NotificationEventType.SearchItemGrabbed;
|
||||
bool onForceImported = eventType == NotificationEventType.ForceImported;
|
||||
bool onErroredStrike = eventType == NotificationEventType.ErroredStrike;
|
||||
bool onSeekerCycleCompleted = eventType == NotificationEventType.SeekerCycleCompleted;
|
||||
|
||||
var config = new NotificationConfig
|
||||
{
|
||||
@@ -212,6 +214,7 @@ public class NotificationConfigurationServiceTests : IDisposable
|
||||
OnSearchItemGrabbed = onSearchItemGrabbed,
|
||||
OnForceImported = onForceImported,
|
||||
OnErroredStrike = onErroredStrike,
|
||||
OnSeekerCycleCompleted = onSeekerCycleCompleted,
|
||||
NotifiarrConfiguration = new NotifiarrConfig
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
|
||||
+56
@@ -505,6 +505,62 @@ public class NotificationPublisherTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region NotifySeekerCycleCompleted Tests
|
||||
|
||||
[Fact]
|
||||
public async Task NotifySeekerCycleCompleted_PublishesNotificationWithCorrectContext()
|
||||
{
|
||||
// Arrange
|
||||
SetupContext(InstanceType.Radarr);
|
||||
|
||||
// Act
|
||||
await _publisher.NotifySeekerCycleCompleted("Radarr Main", TimeSpan.FromHours(5) + TimeSpan.FromMinutes(30), 120, 14, 6);
|
||||
|
||||
// Assert
|
||||
NotificationMessage m = _notificationQueue.ShouldHaveSingle();
|
||||
m.EventType.ShouldBe(NotificationEventType.SeekerCycleCompleted);
|
||||
m.Context.EventType.ShouldBe(NotificationEventType.SeekerCycleCompleted);
|
||||
m.Context.Severity.ShouldBe(EventSeverity.Information);
|
||||
m.Context.Data["Instance"].ShouldBe("Radarr Main");
|
||||
m.Context.Data["Instance type"].ShouldBe("Radarr");
|
||||
m.Context.Data["Url"].ShouldBe("http://sonarr.local/");
|
||||
m.Context.Data["Duration"].ShouldBe("5h 30m");
|
||||
m.Context.Data["Searched"].ShouldBe("120");
|
||||
m.Context.Data["Grabbed"].ShouldBe("14");
|
||||
m.Context.Data["Upgrades grabbed"].ShouldBe("6");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifySeekerCycleCompleted_WithMultiDayDuration_FormatsDaysAndHours()
|
||||
{
|
||||
// Arrange
|
||||
SetupContext();
|
||||
|
||||
// Act
|
||||
await _publisher.NotifySeekerCycleCompleted("Sonarr", TimeSpan.FromDays(3) + TimeSpan.FromHours(4) + TimeSpan.FromMinutes(20), 1, 1, 0);
|
||||
|
||||
// Assert
|
||||
NotificationMessage m = _notificationQueue.ShouldHaveSingle();
|
||||
m.Context.Data["Duration"].ShouldBe("3d 4h");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task NotifySeekerCycleCompleted_WhenExceptionOccurs_LogsError()
|
||||
{
|
||||
// Arrange
|
||||
SetupContext();
|
||||
_dryRunInterceptor.InterceptAsync(Arg.Any<Func<Task>>(), Arg.Any<string?>())
|
||||
.ThrowsAsync(new Exception("Error"));
|
||||
|
||||
// Act
|
||||
await _publisher.NotifySeekerCycleCompleted("Sonarr", TimeSpan.FromHours(1), 1, 1, 0);
|
||||
|
||||
// Assert
|
||||
_logger.HasLogContaining(LogLevel.Error, "Failed to notify seeker cycle completed").ShouldBeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NotifyForceImported Tests
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -8,6 +8,7 @@ using Cleanuparr.Infrastructure.Interceptors;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Cleanuparr.Persistence.Providers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -426,6 +427,36 @@ public class EventPublisher : IEventPublisher
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PublishSeekerCycleCompleted(SeekerCycle cycle, string instanceName)
|
||||
{
|
||||
bool alreadyPublished = await _context.Events
|
||||
.AnyAsync(e => e.EventType == EventType.SeekerCycleCompleted && e.CycleId == cycle.Id);
|
||||
|
||||
if (alreadyPublished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int upgradesGrabbed = await _context.Events
|
||||
.Where(e => e.CycleId == cycle.Id && e.EventType == EventType.SearchTriggered)
|
||||
.Where(e => e.SearchReason == SeekerSearchReason.QualityCutoffNotMet
|
||||
|| e.SearchReason == SeekerSearchReason.CustomFormatScoreBelowCutoff)
|
||||
.Where(e => e.GrabbedItems.Count > 0)
|
||||
.CountAsync();
|
||||
|
||||
TimeSpan duration = (cycle.CompletedAt ?? _timeProvider.GetUtcNow()) - cycle.StartedAt;
|
||||
|
||||
await PublishAsync(
|
||||
EventType.SeekerCycleCompleted,
|
||||
$"Cycle completed on {instanceName} after {cycle.SearchedUnits} searches, {cycle.GrabbedUnits} grabbed",
|
||||
EventSeverity.Information,
|
||||
configure: e => e.CycleId = cycle.Id,
|
||||
isDryRun: cycle.IsDryRun);
|
||||
|
||||
await _notificationPublisher.NotifySeekerCycleCompleted(
|
||||
instanceName, duration, cycle.SearchedUnits, cycle.GrabbedUnits, upgradesGrabbed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing search event with the started status
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
|
||||
namespace Cleanuparr.Infrastructure.Events.Interfaces;
|
||||
|
||||
@@ -76,6 +77,11 @@ public interface IEventPublisher
|
||||
/// </summary>
|
||||
Task PublishSearchStarted(Guid eventId);
|
||||
|
||||
/// <summary>
|
||||
/// Records the first completion of a Seeker cycle and notifies about it. Later completions of the same cycle are ignored.
|
||||
/// </summary>
|
||||
Task PublishSeekerCycleCompleted(SeekerCycle cycle, string instanceName);
|
||||
|
||||
/// <summary>
|
||||
/// Fails each search event of an arr instance that has no terminal status.
|
||||
/// </summary>
|
||||
|
||||
@@ -1406,6 +1406,8 @@ public sealed class Seeker : IHandler
|
||||
|
||||
_logger.LogInformation("Cycle completed on {InstanceName} after {Count} searches",
|
||||
instanceName, cycle.SearchedUnits);
|
||||
|
||||
await _eventPublisher.PublishSeekerCycleCompleted(cycle, instanceName);
|
||||
}
|
||||
|
||||
if (ShouldWaitForMinCycleTime(instanceConfig, cycle.StartedAt))
|
||||
|
||||
+5
@@ -22,4 +22,9 @@ public interface INotificationPublisher
|
||||
Task NotifySearchTriggered(string itemTitle, SeekerSearchType searchType, SeekerSearchReason searchReason);
|
||||
|
||||
Task NotifySearchItemGrabbed(string itemTitle, List<string> grabbedItems, InstanceType instanceType, string instanceUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Reports that Seeker searched every eligible item of an arr instance once.
|
||||
/// </summary>
|
||||
Task NotifySeekerCycleCompleted(string instanceName, TimeSpan duration, int searchedUnits, int grabbedUnits, int upgradesGrabbed);
|
||||
}
|
||||
+2
@@ -23,4 +23,6 @@ public record NotificationEventFlags
|
||||
public bool OnForceImported { get; init; }
|
||||
|
||||
public bool OnErroredStrike { get; init; }
|
||||
|
||||
public bool OnSeekerCycleCompleted { get; init; }
|
||||
}
|
||||
+3
-1
@@ -144,7 +144,8 @@ public sealed class NotificationConfigurationService : INotificationConfiguratio
|
||||
OnSearchTriggered = config.OnSearchTriggered,
|
||||
OnSearchItemGrabbed = config.OnSearchItemGrabbed,
|
||||
OnForceImported = config.OnForceImported,
|
||||
OnErroredStrike = config.OnErroredStrike
|
||||
OnErroredStrike = config.OnErroredStrike,
|
||||
OnSeekerCycleCompleted = config.OnSeekerCycleCompleted
|
||||
};
|
||||
|
||||
var configuration = config.Type switch
|
||||
@@ -185,6 +186,7 @@ public sealed class NotificationConfigurationService : INotificationConfiguratio
|
||||
NotificationEventType.SearchItemGrabbed => events.OnSearchItemGrabbed,
|
||||
NotificationEventType.ForceImported => events.OnForceImported,
|
||||
NotificationEventType.ErroredStrike => events.OnErroredStrike,
|
||||
NotificationEventType.SeekerCycleCompleted => events.OnSeekerCycleCompleted,
|
||||
NotificationEventType.Test => true,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(eventType), $"Provider type {eventType} is not yet registered")
|
||||
};
|
||||
|
||||
+47
@@ -139,6 +139,19 @@ public class NotificationPublisher : INotificationPublisher
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task NotifySeekerCycleCompleted(string instanceName, TimeSpan duration, int searchedUnits, int grabbedUnits, int upgradesGrabbed)
|
||||
{
|
||||
try
|
||||
{
|
||||
NotificationContext context = BuildSeekerCycleCompletedContext(instanceName, duration, searchedUnits, grabbedUnits, upgradesGrabbed);
|
||||
await SendNotificationAsync(NotificationEventType.SeekerCycleCompleted, context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to notify seeker cycle completed");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendNotificationAsync(NotificationEventType eventType, NotificationContext context)
|
||||
{
|
||||
await _dryRunInterceptor.InterceptAsync(() => SendNotificationInternalAsync((eventType, context)));
|
||||
@@ -358,6 +371,40 @@ public class NotificationPublisher : INotificationPublisher
|
||||
};
|
||||
}
|
||||
|
||||
private static NotificationContext BuildSeekerCycleCompletedContext(string instanceName, TimeSpan duration, int searchedUnits, int grabbedUnits, int upgradesGrabbed)
|
||||
{
|
||||
InstanceType instanceType = (InstanceType)ContextProvider.Get<object>(nameof(InstanceType));
|
||||
Uri instanceUrl = ContextProvider.Get<Uri>(ContextProvider.Keys.ArrInstanceUrl);
|
||||
|
||||
return new NotificationContext
|
||||
{
|
||||
EventType = NotificationEventType.SeekerCycleCompleted,
|
||||
Title = "Seeker cycle completed",
|
||||
Description = $"Searched every eligible item on {instanceName} once",
|
||||
Severity = EventSeverity.Information,
|
||||
Data = new Dictionary<string, string>
|
||||
{
|
||||
["Instance"] = instanceName,
|
||||
["Instance type"] = instanceType.ToString(),
|
||||
["Url"] = instanceUrl.ToString(),
|
||||
["Duration"] = FormatDuration(duration),
|
||||
["Searched"] = searchedUnits.ToString(CultureInfo.InvariantCulture),
|
||||
["Grabbed"] = grabbedUnits.ToString(CultureInfo.InvariantCulture),
|
||||
["Upgrades grabbed"] = upgradesGrabbed.ToString(CultureInfo.InvariantCulture),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatDuration(TimeSpan duration)
|
||||
{
|
||||
if (duration.TotalDays >= 1)
|
||||
{
|
||||
return $"{(int)duration.TotalDays}d {duration.Hours}h";
|
||||
}
|
||||
|
||||
return $"{duration.Hours}h {duration.Minutes}m";
|
||||
}
|
||||
|
||||
private static NotificationEventType MapStrikeTypeToEventType(StrikeType strikeType)
|
||||
{
|
||||
return strikeType switch
|
||||
|
||||
+2071
File diff suppressed because it is too large.
Load diff
+31
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Postgres.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSeekerCycleCompletedNotification : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "on_seeker_cycle_completed",
|
||||
schema: "data",
|
||||
table: "notification_configs",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "on_seeker_cycle_completed",
|
||||
schema: "data",
|
||||
table: "notification_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -1217,6 +1217,10 @@ namespace Cleanuparr.Persistence.Postgres.Migrations.Data
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("on_search_triggered");
|
||||
|
||||
b.Property<bool>("OnSeekerCycleCompleted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("on_seeker_cycle_completed");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
+2067
File diff suppressed because it is too large.
Load diff
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Data
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSeekerCycleCompletedNotification : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "on_seeker_cycle_completed",
|
||||
table: "notification_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "on_seeker_cycle_completed",
|
||||
table: "notification_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1212,6 +1212,10 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_search_triggered");
|
||||
|
||||
b.Property<bool>("OnSeekerCycleCompleted")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_seeker_cycle_completed");
|
||||
|
||||
b.Property<bool>("OnSlowStrike")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("on_slow_strike");
|
||||
|
||||
+4
-1
@@ -41,6 +41,8 @@ public sealed record NotificationConfig
|
||||
|
||||
public bool OnErroredStrike { get; init; }
|
||||
|
||||
public bool OnSeekerCycleCompleted { get; init; }
|
||||
|
||||
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
|
||||
public DateTimeOffset UpdatedAt { get; init; } = DateTimeOffset.UtcNow;
|
||||
@@ -84,5 +86,6 @@ public sealed record NotificationConfig
|
||||
OnSearchTriggered ||
|
||||
OnSearchItemGrabbed ||
|
||||
OnForceImported ||
|
||||
OnErroredStrike;
|
||||
OnErroredStrike ||
|
||||
OnSeekerCycleCompleted;
|
||||
}
|
||||
@@ -378,6 +378,7 @@ NotificationConfig MakeNotificationConfig(string name, NotificationProviderType
|
||||
OnQueueItemDeleted = true,
|
||||
OnDownloadCleaned = true,
|
||||
OnSearchItemGrabbed = rng.Next(2) == 0,
|
||||
OnSeekerCycleCompleted = rng.Next(2) == 0,
|
||||
OnCategoryChanged = rng.Next(2) == 0,
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user