added resting units to seeker funnel and preview

This commit is contained in:
Flaminel committed 2026-10-23 03:02:40 +03:00
1 parent 902330e935
commit 0b3a16599d
7 files changed
+190 -5

No files matched your search

@@ -118,6 +118,22 @@ public class SearchStatsControllerTests : IDisposable
funnel.CountedAt.ShouldBe(Now.AddMinutes(-5));
}
[Fact]
public async Task GetSummary_Funnel_SearchedUnitsSubtractsRestingUnits()
{
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
Guid currentCycleId = Guid.NewGuid();
AddInstanceConfig(radarr.Id, currentCycleId);
AddCycle(currentCycleId, radarr.Id, eligibleNow: 10, remainingUnits: 4, startedAt: Now.AddDays(-1),
funnelCountedAt: Now, queuedUnits: 1, recentlyGrabbedUnits: 1, restingUnits: 2);
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
InstanceFunnelStat funnel = summary.PerInstanceStats.ShouldHaveSingleItem().Funnel.ShouldNotBeNull();
funnel.RestingUnits.ShouldBe(2);
funnel.SearchedUnits.ShouldBe(2);
}
[Fact]
public async Task GetSummary_Funnel_SearchedUnitsNeverGoesNegative()
{
@@ -289,7 +305,8 @@ public class SearchStatsControllerTests : IDisposable
int searchedUnits = 0,
DateTimeOffset? funnelCountedAt = null,
int queuedUnits = 0,
int recentlyGrabbedUnits = 0)
int recentlyGrabbedUnits = 0,
int restingUnits = 0)
{
_eventsContext.SeekerCycles.Add(new SeekerCycle
{
@@ -309,6 +326,7 @@ public class SearchStatsControllerTests : IDisposable
TagFilteredItems = 20,
QueuedUnits = queuedUnits,
RecentlyGrabbedUnits = recentlyGrabbedUnits,
RestingUnits = restingUnits,
FunnelCountedAt = funnelCountedAt,
});
_eventsContext.SaveChanges();
@@ -157,7 +157,7 @@ public class SeekerPreviewControllerTests : IDisposable
return sonarr;
}
private ArrInstance AddRadarr(Guid? currentCycleId = null, int minCycleTimeDays = 7)
private ArrInstance AddRadarr(Guid? currentCycleId = null, int minCycleTimeDays = 7, bool restUnitsEnabled = false)
{
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
_dataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
@@ -166,6 +166,7 @@ public class SeekerPreviewControllerTests : IDisposable
Enabled = true,
CurrentCycleId = currentCycleId ?? Guid.NewGuid(),
MinCycleTimeDays = minCycleTimeDays,
RestUnitsEnabled = restUnitsEnabled,
});
_dataContext.SaveChanges();
return radarr;
@@ -247,6 +248,53 @@ public class SeekerPreviewControllerTests : IDisposable
response.Funnel.CountedAt.ShouldBe(Now);
}
[Fact]
public async Task GetPreview_RestingMovie_MapsTheRestFieldsAndCountsItInTheFunnel()
{
Guid cycleId = Guid.NewGuid();
ArrInstance radarr = AddRadarr(cycleId, restUnitsEnabled: true);
AddCycle(cycleId, radarr.Id, Now.AddDays(-1), searchedUnits: 1);
_eventsContext.SeekerHistory.Add(new SeekerHistory
{
ArrInstanceId = radarr.Id,
ExternalItemId = 1,
ItemType = InstanceType.Radarr,
CycleId = Guid.NewGuid(),
LastSearchedAt = Now.AddDays(-2),
ItemTitle = "Movie 1",
ConsecutiveMisses = 4,
});
_eventsContext.SaveChanges();
SetupMovies(Movie(1), Movie(2));
SeekerPreviewResponse response = Preview(await _controller.GetPreview(radarr.Id, CancellationToken.None));
SeekerPreviewExclusionResponse excluded = response.Excluded.ShouldHaveSingleItem();
excluded.Name.ShouldBe("Movie 1");
excluded.Reason.ShouldBe(SeekerExclusionReason.Resting);
excluded.ConsecutiveMisses.ShouldBe(4);
excluded.RestCyclesLeft.ShouldBe(2);
excluded.RestUntil.ShouldBe(Now.AddDays(-2).AddDays(30));
response.NextUp.ShouldHaveSingleItem().Name.ShouldBe("Movie 2");
response.Funnel.RestingUnits.ShouldBe(1);
response.Funnel.SearchedUnits.ShouldBe(0);
response.Funnel.RemainingUnits.ShouldBe(1);
}
[Fact]
public async Task GetPreview_NonRestingExclusion_LeavesTheRestFieldsNull()
{
ArrInstance radarr = AddRadarr();
SetupMovies(Movie(1), Movie(3, status: "announced"));
SeekerPreviewResponse response = Preview(await _controller.GetPreview(radarr.Id, CancellationToken.None));
SeekerPreviewExclusionResponse excluded = response.Excluded.ShouldHaveSingleItem();
excluded.ConsecutiveMisses.ShouldBeNull();
excluded.RestCyclesLeft.ShouldBeNull();
excluded.RestUntil.ShouldBeNull();
}
[Fact]
public async Task GetPreview_QueuedMovie_LeavesItOutOfNextUp()
{
@@ -15,6 +15,7 @@ public sealed record InstanceFunnelStat
public int EligibleUnits { get; init; }
public int QueuedUnits { get; init; }
public int RecentlyGrabbedUnits { get; init; }
public int RestingUnits { get; init; }
public int SearchedUnits { get; init; }
public int RemainingUnits { get; init; }
public DateTimeOffset CountedAt { get; init; }
@@ -33,7 +34,8 @@ public sealed record InstanceFunnelStat
EligibleUnits = cycle.EligibleNow,
QueuedUnits = cycle.QueuedUnits,
RecentlyGrabbedUnits = cycle.RecentlyGrabbedUnits,
SearchedUnits = Math.Max(0, cycle.EligibleNow - cycle.QueuedUnits - cycle.RecentlyGrabbedUnits - cycle.RemainingUnits),
RestingUnits = cycle.RestingUnits,
SearchedUnits = Math.Max(0, cycle.EligibleNow - cycle.QueuedUnits - cycle.RecentlyGrabbedUnits - cycle.RestingUnits - cycle.RemainingUnits),
RemainingUnits = cycle.RemainingUnits,
CountedAt = countedAt,
};
@@ -9,4 +9,19 @@ public sealed record SeekerPreviewExclusionResponse
{
public string Name { get; init; } = string.Empty;
public SeekerExclusionReason Reason { get; init; }
/// <summary>
/// Set while the reason is <see cref="SeekerExclusionReason.Resting"/>, as are the other rest fields.
/// </summary>
public int? ConsecutiveMisses { get; init; }
/// <summary>
/// Cycle ends before the item is searched again.
/// </summary>
public int? RestCyclesLeft { get; init; }
/// <summary>
/// When the rest ends, however many cycles it has left.
/// </summary>
public DateTimeOffset? RestUntil { get; init; }
}
@@ -154,7 +154,14 @@ public sealed class SeekerPreviewController : ControllerBase
.Select(c => new SeekerPreviewUnitResponse { Name = c.Name, Reason = c.Reason, ContentKind = c.ContentKind })
.ToList(),
Excluded = evaluation.Excluded
.Select(e => new SeekerPreviewExclusionResponse { Name = e.Name, Reason = e.Reason })
.Select(e => new SeekerPreviewExclusionResponse
{
Name = e.Name,
Reason = e.Reason,
ConsecutiveMisses = e.Rest?.ConsecutiveMisses,
RestCyclesLeft = e.Rest?.CyclesLeft,
RestUntil = e.Rest?.Until,
})
.ToList(),
});
}
@@ -172,7 +179,8 @@ public sealed class SeekerPreviewController : ControllerBase
EligibleUnits = evaluation.Units.Count,
QueuedUnits = evaluation.QueuedUnits,
RecentlyGrabbedUnits = evaluation.RecentlyGrabbedUnits,
SearchedUnits = evaluation.Units.Count - evaluation.QueuedUnits - evaluation.RecentlyGrabbedUnits - remaining,
RestingUnits = evaluation.RestingUnits,
SearchedUnits = Math.Max(0, evaluation.Units.Count - evaluation.QueuedUnits - evaluation.RecentlyGrabbedUnits - evaluation.RestingUnits - remaining),
RemainingUnits = remaining,
CountedAt = _timeProvider.GetUtcNow(),
};
@@ -3108,6 +3108,99 @@ public class SeekerTests : IDisposable
newCycle.FunnelCountedAt.ShouldBe(_fixture.TimeProvider.GetUtcNow());
}
private void AddRestingMovieHistory(ArrInstance instance, long movieId, Guid cycleId)
{
_fixture.EventsContext.SeekerHistory.Add(new SeekerHistory
{
ArrInstanceId = instance.Id,
ExternalItemId = movieId,
ItemType = InstanceType.Radarr,
CycleId = cycleId,
LastSearchedAt = _fixture.TimeProvider.GetUtcNow().AddDays(-2),
ItemTitle = $"Movie {movieId}",
ConsecutiveMisses = 3,
});
}
[Fact]
public async Task ExecuteAsync_Radarr_RestingMovie_RecordsRestingUnitsOnTheCurrentCycle()
{
// Arrange
(ArrInstance radarrInstance, _) = await SetupFunnelInstanceAsync(InstanceType.Radarr, c => c.RestUnitsEnabled = true);
AddRestingMovieHistory(radarrInstance, 1, Guid.NewGuid());
await _fixture.EventsContext.SaveChangesAsync();
StubFunnelMovies(radarrInstance, [ReleasedMovie(1), ReleasedMovie(2), ReleasedMovie(3)]);
// Act
await CreateSut().ExecuteAsync();
// Assert
SeekerCycle cycle = await GetCycleAsync(radarrInstance.Id);
cycle.RestingUnits.ShouldBe(1);
cycle.QueuedUnits.ShouldBe(0);
}
[Fact]
public async Task ExecuteAsync_Radarr_EveryUnsearchedUnitRests_TriggersNoSearch()
{
// Arrange: movie 1 was searched in the open cycle, movies 2 and 3 rest
Guid currentCycleId = Guid.NewGuid();
(ArrInstance radarrInstance, IArrClient arrClient) = await SetupFunnelInstanceAsync(InstanceType.Radarr, c =>
{
c.RestUnitsEnabled = true;
c.CurrentCycleId = currentCycleId;
});
_fixture.EventsContext.SeekerCycles.Add(new SeekerCycle
{
Id = currentCycleId,
ArrInstanceId = radarrInstance.Id,
StartedAt = _fixture.TimeProvider.GetUtcNow().AddHours(-1),
SearchedUnits = 1,
});
_fixture.EventsContext.SeekerHistory.Add(new SeekerHistory
{
ArrInstanceId = radarrInstance.Id,
ExternalItemId = 1,
ItemType = InstanceType.Radarr,
CycleId = currentCycleId,
LastSearchedAt = _fixture.TimeProvider.GetUtcNow().AddHours(-1),
ItemTitle = "Movie 1",
});
AddRestingMovieHistory(radarrInstance, 2, Guid.NewGuid());
AddRestingMovieHistory(radarrInstance, 3, Guid.NewGuid());
await _fixture.EventsContext.SaveChangesAsync();
StubFunnelMovies(radarrInstance, [ReleasedMovie(1), ReleasedMovie(2), ReleasedMovie(3)]);
// Act
await CreateSut().ExecuteAsync();
// Assert
await arrClient.DidNotReceive().SearchItemAsync(radarrInstance, Arg.Any<SearchItem>());
}
[Fact]
public async Task ExecuteAsync_Radarr_EveryUnitRestsAndNoneWasSearched_DoesNotCompleteTheCycle()
{
// Arrange
(ArrInstance radarrInstance, IArrClient arrClient) = await SetupFunnelInstanceAsync(InstanceType.Radarr, c => c.RestUnitsEnabled = true);
AddRestingMovieHistory(radarrInstance, 1, Guid.NewGuid());
AddRestingMovieHistory(radarrInstance, 2, Guid.NewGuid());
await _fixture.EventsContext.SaveChangesAsync();
StubFunnelMovies(radarrInstance, [ReleasedMovie(1), ReleasedMovie(2)]);
// Act
await CreateSut().ExecuteAsync();
// Assert
await arrClient.DidNotReceive().SearchItemAsync(radarrInstance, Arg.Any<SearchItem>());
await _fixture.EventPublisher.DidNotReceive()
.PublishSeekerCycleCompleted(Arg.Any<SeekerCycle>(), Arg.Any<string>());
SeekerCycle cycle = await GetCycleAsync(radarrInstance.Id);
cycle.RestingUnits.ShouldBe(2);
cycle.CompletedAt.ShouldBeNull();
}
#endregion
#region Post-Release Grace Period Tests
@@ -591,6 +591,7 @@ public sealed class Seeker : IHandler
cycle.TagFilteredItems = evaluation.TagFilteredItems;
cycle.QueuedUnits = evaluation.QueuedUnits;
cycle.RecentlyGrabbedUnits = evaluation.RecentlyGrabbedUnits;
cycle.RestingUnits = evaluation.RestingUnits;
cycle.FunnelCountedAt = _timeProvider.GetUtcNow();
}