mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
changed cycle progress to count search units
This commit is contained in:
1 parent
af1823779a
commit
081d50ca1b
13 files changed
+4402
-68
No files matched your search
@@ -4,8 +4,12 @@ using Cleanuparr.Api.Features.Seeker.Controllers;
|
||||
using Cleanuparr.Api.Tests.Features.Seeker.TestHelpers;
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Seeker;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using Shouldly;
|
||||
|
||||
@@ -74,6 +78,123 @@ public class SearchStatsControllerTests : IDisposable
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetSummary PerInstanceStats
|
||||
|
||||
[Fact]
|
||||
public async Task GetSummary_PerInstanceStats_UsesTheCurrentCycleRow()
|
||||
{
|
||||
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
|
||||
Guid currentCycleId = Guid.NewGuid();
|
||||
AddInstanceConfig(radarr.Id, currentCycleId);
|
||||
AddCycle(currentCycleId, radarr.Id, eligibleNow: 10, remainingUnits: 4, startedAt: Now.AddDays(-2));
|
||||
|
||||
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
|
||||
|
||||
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
|
||||
stat.CycleItemsTotal.ShouldBe(10);
|
||||
stat.CycleItemsRemaining.ShouldBe(4);
|
||||
stat.CycleItemsSearched.ShouldBe(6);
|
||||
stat.CycleStartedAt.ShouldBe(Now.AddDays(-2));
|
||||
stat.CycleCompletedAt.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSummary_PerInstanceStats_LastCycle_IsTheNewestCompletedOtherCycle()
|
||||
{
|
||||
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
|
||||
Guid currentCycleId = Guid.NewGuid();
|
||||
AddInstanceConfig(radarr.Id, currentCycleId);
|
||||
AddCycle(currentCycleId, radarr.Id, eligibleNow: 5, remainingUnits: 5, startedAt: Now);
|
||||
|
||||
Guid olderCompletedId = Guid.NewGuid();
|
||||
Guid newerCompletedId = Guid.NewGuid();
|
||||
AddCycle(olderCompletedId, radarr.Id, eligibleNow: 3, remainingUnits: 0, startedAt: Now.AddDays(-20), completedAt: Now.AddDays(-19));
|
||||
AddCycle(newerCompletedId, radarr.Id, eligibleNow: 3, remainingUnits: 0, startedAt: Now.AddDays(-10), completedAt: Now.AddDays(-9));
|
||||
|
||||
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
|
||||
|
||||
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
|
||||
stat.LastCycleStartedAt.ShouldBe(Now.AddDays(-10));
|
||||
stat.LastCycleCompletedAt.ShouldBe(Now.AddDays(-9));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSummary_PerInstanceStats_DryRunOn_ShowsNewestDryCycleAndIgnoresLiveCycles()
|
||||
{
|
||||
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
|
||||
(await _dataContext.GeneralConfigs.FirstAsync()).DryRun = true;
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
Guid currentCycleId = Guid.NewGuid();
|
||||
AddInstanceConfig(radarr.Id, currentCycleId);
|
||||
AddCycle(currentCycleId, radarr.Id, eligibleNow: 99, remainingUnits: 99, startedAt: Now.AddDays(-30), isDryRun: false);
|
||||
|
||||
Guid dryCycleId = Guid.NewGuid();
|
||||
AddCycle(dryCycleId, radarr.Id, eligibleNow: 7, remainingUnits: 2, startedAt: Now.AddHours(-1), isDryRun: true);
|
||||
|
||||
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
|
||||
|
||||
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
|
||||
stat.CurrentCycleId.ShouldBe(dryCycleId);
|
||||
stat.CycleItemsTotal.ShouldBe(7);
|
||||
stat.CycleItemsRemaining.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSummary_PerInstanceStats_NoCycleRow_ValuesAreZeroOrNull()
|
||||
{
|
||||
var radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
|
||||
AddInstanceConfig(radarr.Id, Guid.NewGuid());
|
||||
|
||||
SearchStatsSummaryResponse summary = Summary(await _controller.GetSummary());
|
||||
|
||||
InstanceSearchStat stat = summary.PerInstanceStats.ShouldHaveSingleItem();
|
||||
stat.CycleItemsSearched.ShouldBe(0);
|
||||
stat.CycleItemsTotal.ShouldBe(0);
|
||||
stat.CycleItemsRemaining.ShouldBe(0);
|
||||
stat.CycleStartedAt.ShouldBeNull();
|
||||
stat.CycleCompletedAt.ShouldBeNull();
|
||||
stat.LastCycleStartedAt.ShouldBeNull();
|
||||
stat.LastCycleCompletedAt.ShouldBeNull();
|
||||
}
|
||||
|
||||
private void AddInstanceConfig(Guid arrInstanceId, Guid currentCycleId)
|
||||
{
|
||||
_dataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = arrInstanceId,
|
||||
Enabled = true,
|
||||
CurrentCycleId = currentCycleId,
|
||||
});
|
||||
_dataContext.SaveChanges();
|
||||
}
|
||||
|
||||
private void AddCycle(
|
||||
Guid id,
|
||||
Guid arrInstanceId,
|
||||
int eligibleNow,
|
||||
int remainingUnits,
|
||||
DateTimeOffset startedAt,
|
||||
DateTimeOffset? completedAt = null,
|
||||
bool isDryRun = false)
|
||||
{
|
||||
_eventsContext.SeekerCycles.Add(new SeekerCycle
|
||||
{
|
||||
Id = id,
|
||||
ArrInstanceId = arrInstanceId,
|
||||
StartedAt = startedAt,
|
||||
CompletedAt = completedAt,
|
||||
CompletionReason = completedAt is null ? null : SeekerCycleCompletionReason.Completed,
|
||||
EligibleAtStart = eligibleNow,
|
||||
EligibleNow = eligibleNow,
|
||||
RemainingUnits = remainingUnits,
|
||||
IsDryRun = isDryRun,
|
||||
});
|
||||
_eventsContext.SaveChanges();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetEvents with SearchEventData
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -12,5 +12,9 @@ public sealed record InstanceSearchStat
|
||||
public Guid? CurrentCycleId { get; init; }
|
||||
public int CycleItemsSearched { get; init; }
|
||||
public int CycleItemsTotal { get; init; }
|
||||
public int CycleItemsRemaining { get; init; }
|
||||
public DateTimeOffset? CycleStartedAt { get; init; }
|
||||
public DateTimeOffset? CycleCompletedAt { get; init; }
|
||||
public DateTimeOffset? LastCycleStartedAt { get; init; }
|
||||
public DateTimeOffset? LastCycleCompletedAt { get; init; }
|
||||
}
|
||||
@@ -75,24 +75,25 @@ public sealed class SearchStatsController : ControllerBase
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
// Count items searched in current cycle per instance
|
||||
List<Guid> currentCycleIds = instanceConfigs.Select(ic => ic.CurrentCycleId).ToList();
|
||||
var cycleItemsByInstance = await _eventsContext.SeekerHistory
|
||||
// Dry run searches in its own cycles, so show those while it is on.
|
||||
bool isDryRun = (await _dataContext.GeneralConfigs.AsNoTracking().FirstAsync()).DryRun;
|
||||
List<Guid> instanceIds = instanceConfigs.Select(ic => ic.ArrInstanceId).ToList();
|
||||
List<SeekerCycle> cycles = await _eventsContext.SeekerCycles
|
||||
.AsNoTracking()
|
||||
.Where(h => currentCycleIds.Contains(h.CycleId))
|
||||
.GroupBy(h => h.ArrInstanceId)
|
||||
.Select(g => new
|
||||
{
|
||||
InstanceId = g.Key,
|
||||
CycleItemsSearched = g.Select(h => h.ExternalItemId).Distinct().Count(),
|
||||
CycleStartedAt = (DateTimeOffset?)g.Min(h => h.LastSearchedAt),
|
||||
})
|
||||
.Where(c => instanceIds.Contains(c.ArrInstanceId) && c.IsDryRun == isDryRun)
|
||||
.ToListAsync();
|
||||
|
||||
var perInstanceStats = instanceConfigs.Select(ic =>
|
||||
{
|
||||
var history = historyByInstance.FirstOrDefault(h => h.InstanceId == ic.ArrInstanceId);
|
||||
var cycleProgress = cycleItemsByInstance.FirstOrDefault(c => c.InstanceId == ic.ArrInstanceId);
|
||||
List<SeekerCycle> instanceCycles = cycles
|
||||
.Where(c => c.ArrInstanceId == ic.ArrInstanceId)
|
||||
.OrderByDescending(c => c.StartedAt)
|
||||
.ToList();
|
||||
SeekerCycle? current = isDryRun
|
||||
? instanceCycles.FirstOrDefault()
|
||||
: instanceCycles.FirstOrDefault(c => c.Id == ic.CurrentCycleId);
|
||||
SeekerCycle? last = instanceCycles.FirstOrDefault(c => c.CompletedAt != null && c.Id != current?.Id);
|
||||
return new InstanceSearchStat
|
||||
{
|
||||
InstanceId = ic.ArrInstanceId,
|
||||
@@ -102,10 +103,14 @@ public sealed class SearchStatsController : ControllerBase
|
||||
TotalSearchCount = history?.TotalSearchCount ?? 0,
|
||||
LastSearchedAt = history?.LastSearchedAt,
|
||||
LastProcessedAt = ic.LastProcessedAt,
|
||||
CurrentCycleId = ic.CurrentCycleId,
|
||||
CycleItemsSearched = cycleProgress?.CycleItemsSearched ?? 0,
|
||||
CycleItemsTotal = ic.TotalEligibleItems,
|
||||
CycleStartedAt = cycleProgress?.CycleStartedAt,
|
||||
CurrentCycleId = current?.Id ?? ic.CurrentCycleId,
|
||||
CycleItemsSearched = current is null ? 0 : Math.Max(0, current.EligibleNow - current.RemainingUnits),
|
||||
CycleItemsTotal = current?.EligibleNow ?? 0,
|
||||
CycleItemsRemaining = current?.RemainingUnits ?? 0,
|
||||
CycleStartedAt = current?.StartedAt,
|
||||
CycleCompletedAt = current?.CompletedAt,
|
||||
LastCycleStartedAt = last?.StartedAt,
|
||||
LastCycleCompletedAt = last?.CompletedAt,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
|
||||
@@ -1772,8 +1772,7 @@ public class SeekerTests : IDisposable
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 2
|
||||
MinCycleTimeDays = 7
|
||||
});
|
||||
|
||||
// Cycle started 2 days ago — within the 7-day minimum
|
||||
@@ -1855,8 +1854,7 @@ public class SeekerTests : IDisposable
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 2
|
||||
MinCycleTimeDays = 7
|
||||
});
|
||||
|
||||
// Cycle started 10 days ago — beyond the 7-day minimum
|
||||
@@ -2013,8 +2011,7 @@ public class SeekerTests : IDisposable
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 1
|
||||
MinCycleTimeDays = 7
|
||||
});
|
||||
|
||||
// Series history — season already searched in current cycle (started 2 days ago)
|
||||
@@ -2089,8 +2086,7 @@ public class SeekerTests : IDisposable
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 1
|
||||
MinCycleTimeDays = 7
|
||||
});
|
||||
|
||||
// Series history — season already searched in current cycle (started 10 days ago)
|
||||
@@ -2174,7 +2170,6 @@ public class SeekerTests : IDisposable
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = cycleIdA,
|
||||
MinCycleTimeDays = 30,
|
||||
TotalEligibleItems = 1,
|
||||
LastProcessedAt = now.AddDays(-5) // Oldest — round-robin tries this first
|
||||
});
|
||||
_fixture.EventsContext.SeekerHistory.Add(new SeekerHistory
|
||||
@@ -2198,7 +2193,6 @@ public class SeekerTests : IDisposable
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = cycleIdB,
|
||||
MinCycleTimeDays = 5,
|
||||
TotalEligibleItems = 1,
|
||||
LastProcessedAt = now.AddDays(-1)
|
||||
});
|
||||
// No history for instance B — it hasn't searched anything yet
|
||||
@@ -2281,8 +2275,7 @@ public class SeekerTests : IDisposable
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 30,
|
||||
TotalEligibleItems = 2 // Stale value from previous run
|
||||
MinCycleTimeDays = 30
|
||||
});
|
||||
|
||||
// History: 2 items searched in current cycle
|
||||
@@ -2373,8 +2366,7 @@ public class SeekerTests : IDisposable
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 30,
|
||||
TotalEligibleItems = 2 // Stale value from previous run
|
||||
MinCycleTimeDays = 30
|
||||
});
|
||||
|
||||
// History: items 1 and 2 were searched
|
||||
@@ -4618,9 +4610,9 @@ public class SeekerTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Sonarr_TotalEligibleItems_CountsSeriesWithAnEligibleSeason()
|
||||
public async Task ExecuteAsync_Sonarr_Cycle_EligibleNow_CountsEligibleSeasonsAcrossSeries()
|
||||
{
|
||||
// Arrange: series 10 has two eligible seasons, series 20 has one; TotalEligibleItems counts series, not seasons
|
||||
// Arrange: series 10 has two eligible seasons, series 20 has one; the cycle counts seasons, not series
|
||||
var config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
@@ -4673,10 +4665,10 @@ public class SeekerTests : IDisposable
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert: two series had an eligible season, even though series 10 contributed two seasons
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == sonarrInstance.Id);
|
||||
instanceConfig.TotalEligibleItems.ShouldBe(2);
|
||||
// Assert
|
||||
var cycle = await _fixture.EventsContext.SeekerCycles
|
||||
.FirstAsync(c => c.ArrInstanceId == sonarrInstance.Id && !c.IsDryRun);
|
||||
cycle.EligibleNow.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -5236,7 +5228,7 @@ public class SeekerTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Radarr_UpdatesTotalEligibleItems()
|
||||
public async Task ExecuteAsync_Radarr_Cycle_EligibleNow_UpdatesToMatchEligibleMovies()
|
||||
{
|
||||
// Arrange
|
||||
var config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
@@ -5252,8 +5244,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
TotalEligibleItems = 0 // Start at 0
|
||||
MonitoredOnly = false
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
@@ -5291,10 +5282,10 @@ public class SeekerTests : IDisposable
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert — TotalEligibleItems updated to 3
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == radarrInstance.Id);
|
||||
instanceConfig.TotalEligibleItems.ShouldBe(3);
|
||||
// Assert
|
||||
var cycle = await _fixture.EventsContext.SeekerCycles
|
||||
.FirstAsync(c => c.ArrInstanceId == radarrInstance.Id && !c.IsDryRun);
|
||||
cycle.EligibleNow.ShouldBe(3);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -7338,10 +7329,6 @@ public class SeekerTests : IDisposable
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == sonarrInstance.Id);
|
||||
instanceConfig.TotalEligibleItems.ShouldBe(1);
|
||||
|
||||
var cycle = await _fixture.EventsContext.SeekerCycles
|
||||
.FirstAsync(c => c.ArrInstanceId == sonarrInstance.Id && !c.IsDryRun);
|
||||
cycle.EligibleNow.ShouldBe(2);
|
||||
|
||||
@@ -644,7 +644,6 @@ public sealed class Seeker : IHandler
|
||||
}
|
||||
|
||||
int eligibleCount = candidates.Count;
|
||||
instanceConfig.TotalEligibleItems = eligibleCount;
|
||||
cycle.EligibleNow = eligibleCount;
|
||||
cycle.RemainingUnits = 0;
|
||||
|
||||
@@ -842,7 +841,6 @@ public sealed class Seeker : IHandler
|
||||
.GroupBy(q => (q.SeriesId, q.SeasonNumber))
|
||||
.ToDictionary(g => g.Key, g => g.Select(q => q.Reason).ToList());
|
||||
|
||||
instanceConfig.TotalEligibleItems = seasons.Keys.Select(s => s.SeriesId).Distinct().Count();
|
||||
cycle.EligibleNow = seasons.Count;
|
||||
cycle.RemainingUnits = 0;
|
||||
|
||||
|
||||
+2067
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 RemoveSeekerTotalEligibleItems : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "total_eligible_items",
|
||||
schema: "data",
|
||||
table: "seeker_instance_configs");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "total_eligible_items",
|
||||
schema: "data",
|
||||
table: "seeker_instance_configs",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
-4
@@ -1740,10 +1740,6 @@ namespace Cleanuparr.Persistence.Postgres.Migrations.Data
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("skip_tags");
|
||||
|
||||
b.Property<int>("TotalEligibleItems")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("total_eligible_items");
|
||||
|
||||
b.Property<bool>("UseCustomFormatScore")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("use_custom_format_score");
|
||||
|
||||
+2063
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 RemoveSeekerTotalEligibleItems : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "total_eligible_items",
|
||||
table: "seeker_instance_configs");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "total_eligible_items",
|
||||
table: "seeker_instance_configs",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1736,10 +1736,6 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("skip_tags");
|
||||
|
||||
b.Property<int>("TotalEligibleItems")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("total_eligible_items");
|
||||
|
||||
b.Property<bool>("UseCustomFormatScore")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("use_custom_format_score");
|
||||
|
||||
-6
@@ -52,12 +52,6 @@ public sealed record SeekerInstanceConfig
|
||||
/// </summary>
|
||||
public Guid CurrentCycleId { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Total number of eligible items in the library for this instance.
|
||||
/// Updated each time the Seeker processes the instance.
|
||||
/// </summary>
|
||||
public int TotalEligibleItems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Skip proactive search cycles when the number of actively downloading items
|
||||
/// (SizeLeft > 0) in the arr queue is at or above this threshold. 0 = disabled.
|
||||
|
||||
@@ -607,7 +607,9 @@ Console.WriteLine($"Built a media pool of {mediaPool.Count} distinct titles.");
|
||||
// ---- Seeker per-instance configs: capacity-driven so "cycle progress" and "cycle duration"
|
||||
// on the Seeker Stats page show real, non-zero values instead of "0 / N" and "-" ----
|
||||
List<SeekerHistory> seekerHistory = [];
|
||||
HashSet<(Guid, long, InstanceType, int, Guid)> seekerHistoryKeys = [];
|
||||
List<SeekerCycle> seekerCycles = [];
|
||||
// Matches the SeekerHistory unique index; EpisodeId and IsDryRun are always 0/false here.
|
||||
HashSet<(Guid, long, InstanceType, int)> seekerHistoryKeys = [];
|
||||
|
||||
// Generic filler titles for library padding: real curated titles stay scarce (they're the
|
||||
// ones the docs gallery pool knows by name), so background capacity/cycle rows that exist
|
||||
@@ -659,6 +661,9 @@ foreach (ArrInstance instance in seekerInstances)
|
||||
List<DateTimeOffset> curatedTimestamps = cycleTimestampsDesc.Take(cycleItemIds.Count).OrderBy(_ => rng.Next()).ToList();
|
||||
List<DateTimeOffset> fillerTimestamps = cycleTimestampsDesc.Skip(cycleItemIds.Count).OrderBy(_ => rng.Next()).ToList();
|
||||
|
||||
int searchedUnits = 0;
|
||||
DateTimeOffset cycleStartedAt = now;
|
||||
|
||||
for (int i = 0; i < cycleItemIds.Count; i++)
|
||||
{
|
||||
long externalId = cycleItemIds[i];
|
||||
@@ -666,13 +671,19 @@ foreach (ArrInstance instance in seekerInstances)
|
||||
? mediaPool.First(m => m.Instance?.Id == instance.Id && m.ExternalItemId == externalId).SeasonNumber
|
||||
: 0;
|
||||
string title = instanceType == InstanceType.Sonarr ? sonarrSeriesTitle[externalId] : radarrMovieTitle[externalId];
|
||||
(Guid, long, InstanceType, int, Guid) key = (instance.Id, externalId, instanceType, seasonNumber, currentCycleId);
|
||||
(Guid, long, InstanceType, int) key = (instance.Id, externalId, instanceType, seasonNumber);
|
||||
|
||||
if (!seekerHistoryKeys.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
searchedUnits++;
|
||||
if (curatedTimestamps[i] < cycleStartedAt)
|
||||
{
|
||||
cycleStartedAt = curatedTimestamps[i];
|
||||
}
|
||||
|
||||
seekerHistory.Add(new SeekerHistory
|
||||
{
|
||||
ArrInstanceId = instance.Id,
|
||||
@@ -692,13 +703,19 @@ foreach (ArrInstance instance in seekerInstances)
|
||||
{
|
||||
long fillerId = 100_000 + f;
|
||||
int seasonNumber = instanceType == InstanceType.Sonarr ? 1 : 0;
|
||||
(Guid, long, InstanceType, int, Guid) key = (instance.Id, fillerId, instanceType, seasonNumber, currentCycleId);
|
||||
(Guid, long, InstanceType, int) key = (instance.Id, fillerId, instanceType, seasonNumber);
|
||||
|
||||
if (!seekerHistoryKeys.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
searchedUnits++;
|
||||
if (fillerTimestamps[f] < cycleStartedAt)
|
||||
{
|
||||
cycleStartedAt = fillerTimestamps[f];
|
||||
}
|
||||
|
||||
seekerHistory.Add(new SeekerHistory
|
||||
{
|
||||
ArrInstanceId = instance.Id,
|
||||
@@ -721,9 +738,34 @@ foreach (ArrInstance instance in seekerInstances)
|
||||
MonitoredOnly = true,
|
||||
ActiveDownloadLimit = 3,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = capacity,
|
||||
CurrentCycleId = currentCycleId,
|
||||
});
|
||||
|
||||
seekerCycles.Add(new SeekerCycle
|
||||
{
|
||||
Id = currentCycleId,
|
||||
ArrInstanceId = instance.Id,
|
||||
StartedAt = cycleStartedAt,
|
||||
EligibleAtStart = capacity,
|
||||
EligibleNow = capacity,
|
||||
SearchedUnits = searchedUnits,
|
||||
RemainingUnits = Math.Max(0, capacity - searchedUnits),
|
||||
});
|
||||
|
||||
// A closed previous cycle, a few days long, ending before the current one started.
|
||||
DateTimeOffset previousCompletedAt = cycleStartedAt.AddDays(-rng.Next(1, 4));
|
||||
seekerCycles.Add(new SeekerCycle
|
||||
{
|
||||
ArrInstanceId = instance.Id,
|
||||
StartedAt = previousCompletedAt.AddDays(-rng.Next(3, 8)),
|
||||
CompletedAt = previousCompletedAt,
|
||||
CompletionReason = SeekerCycleCompletionReason.Completed,
|
||||
EligibleAtStart = capacity,
|
||||
EligibleNow = capacity,
|
||||
SearchedUnits = capacity,
|
||||
RemainingUnits = 0,
|
||||
GrabbedUnits = rng.Next(0, capacity / 3 + 1),
|
||||
});
|
||||
}
|
||||
await data.SaveChangesAsync();
|
||||
Console.WriteLine($"Seeded {dataDbPath}: 4 seeker instance configs (capacity-driven cycle progress).");
|
||||
@@ -739,7 +781,7 @@ for (int i = 0; i < pastCycleHistoryCount; i++)
|
||||
|
||||
MediaItem media = Pick(sonarrRadarrForHistory);
|
||||
Guid pastCycleId = Guid.NewGuid();
|
||||
(Guid, long, InstanceType, int, Guid) key = (media.Instance!.Id, media.ExternalItemId, media.Type!.Value, media.SeasonNumber, pastCycleId);
|
||||
(Guid, long, InstanceType, int) key = (media.Instance!.Id, media.ExternalItemId, media.Type!.Value, media.SeasonNumber);
|
||||
|
||||
if (!seekerHistoryKeys.Add(key))
|
||||
{
|
||||
@@ -760,6 +802,7 @@ for (int i = 0; i < pastCycleHistoryCount; i++)
|
||||
});
|
||||
}
|
||||
events.SeekerHistory.AddRange(seekerHistory);
|
||||
events.SeekerCycles.AddRange(seekerCycles);
|
||||
await events.SaveChangesAsync();
|
||||
|
||||
// ---- Job runs: capped, recency/diurnal-biased sample per JobType (not literal cron replay,
|
||||
|
||||
Reference in new issue
Block a user