mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-03-25 09:31:38 -04:00
fixed cycle id naming inconsistency
This commit is contained in:
@@ -9,7 +9,7 @@ public sealed record InstanceSearchStat
|
||||
public int TotalSearchCount { get; init; }
|
||||
public DateTime? LastSearchedAt { get; init; }
|
||||
public DateTime? LastProcessedAt { get; init; }
|
||||
public Guid? CurrentRunId { get; init; }
|
||||
public Guid? CurrentCycleId { get; init; }
|
||||
public int CycleItemsSearched { get; init; }
|
||||
public int CycleItemsTotal { get; init; }
|
||||
public DateTime? CycleStartedAt { get; init; }
|
||||
|
||||
@@ -14,6 +14,6 @@ public sealed record SearchEventResponse
|
||||
public SearchCommandStatus? SearchStatus { get; init; }
|
||||
public DateTime? CompletedAt { get; init; }
|
||||
public object? GrabbedItems { get; init; }
|
||||
public Guid? CycleRunId { get; init; }
|
||||
public Guid? CycleId { get; init; }
|
||||
public bool IsDryRun { get; init; }
|
||||
}
|
||||
|
||||
@@ -72,10 +72,10 @@ public sealed class SearchStatsController : ControllerBase
|
||||
.ToListAsync();
|
||||
|
||||
// Count items searched in current cycle per instance
|
||||
List<Guid> currentRunIds = instanceConfigs.Select(ic => ic.CurrentRunId).ToList();
|
||||
List<Guid> currentCycleIds = instanceConfigs.Select(ic => ic.CurrentCycleId).ToList();
|
||||
var cycleItemsByInstance = await _dataContext.SeekerHistory
|
||||
.AsNoTracking()
|
||||
.Where(h => currentRunIds.Contains(h.RunId))
|
||||
.Where(h => currentCycleIds.Contains(h.CycleId))
|
||||
.GroupBy(h => h.ArrInstanceId)
|
||||
.Select(g => new
|
||||
{
|
||||
@@ -98,7 +98,7 @@ public sealed class SearchStatsController : ControllerBase
|
||||
TotalSearchCount = history?.TotalSearchCount ?? 0,
|
||||
LastSearchedAt = history?.LastSearchedAt,
|
||||
LastProcessedAt = ic.LastProcessedAt,
|
||||
CurrentRunId = ic.CurrentRunId,
|
||||
CurrentCycleId = ic.CurrentCycleId,
|
||||
CycleItemsSearched = cycleProgress?.CycleItemsSearched ?? 0,
|
||||
CycleItemsTotal = ic.TotalEligibleItems,
|
||||
CycleStartedAt = cycleProgress?.CycleStartedAt,
|
||||
@@ -205,7 +205,7 @@ public sealed class SearchStatsController : ControllerBase
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] Guid? instanceId = null,
|
||||
[FromQuery] Guid? cycleRunId = null)
|
||||
[FromQuery] Guid? cycleId = null)
|
||||
{
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1) pageSize = 50;
|
||||
@@ -229,10 +229,10 @@ public sealed class SearchStatsController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by cycle run ID
|
||||
if (cycleRunId.HasValue)
|
||||
// Filter by cycle ID
|
||||
if (cycleId.HasValue)
|
||||
{
|
||||
query = query.Where(e => e.CycleRunId == cycleRunId.Value);
|
||||
query = query.Where(e => e.CycleId == cycleId.Value);
|
||||
}
|
||||
|
||||
int totalCount = await query.CountAsync();
|
||||
@@ -258,7 +258,7 @@ public sealed class SearchStatsController : ControllerBase
|
||||
SearchStatus = e.SearchStatus,
|
||||
CompletedAt = e.CompletedAt,
|
||||
GrabbedItems = parsed.GrabbedItems,
|
||||
CycleRunId = e.CycleRunId,
|
||||
CycleId = e.CycleId,
|
||||
IsDryRun = e.IsDryRun,
|
||||
};
|
||||
}).ToList();
|
||||
@@ -344,7 +344,7 @@ public sealed class SearchStatsController : ControllerBase
|
||||
SearchStatus = e.SearchStatus,
|
||||
CompletedAt = e.CompletedAt,
|
||||
GrabbedItems = parsed.GrabbedItems,
|
||||
CycleRunId = e.CycleRunId,
|
||||
CycleId = e.CycleId,
|
||||
IsDryRun = e.IsDryRun,
|
||||
}
|
||||
: null;
|
||||
|
||||
@@ -608,18 +608,18 @@ public class EventPublisherTests : IDisposable
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PublishSearchTriggered_SetsCycleRunId()
|
||||
public async Task PublishSearchTriggered_SetsCycleId()
|
||||
{
|
||||
// Arrange
|
||||
var cycleRunId = Guid.NewGuid();
|
||||
var cycleId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
await _publisher.PublishSearchTriggered("Radarr-1", 1, ["Movie A"], SeekerSearchType.Proactive, cycleRunId);
|
||||
await _publisher.PublishSearchTriggered("Radarr-1", 1, ["Movie A"], SeekerSearchType.Proactive, cycleId);
|
||||
|
||||
// Assert
|
||||
var savedEvent = await _context.Events.FirstOrDefaultAsync();
|
||||
Assert.NotNull(savedEvent);
|
||||
Assert.Equal(cycleRunId, savedEvent.CycleRunId);
|
||||
Assert.Equal(cycleId, savedEvent.CycleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -717,7 +717,7 @@ public class SeekerTests : IDisposable
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
|
||||
var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
var currentRunId = Guid.NewGuid();
|
||||
var currentCycleId = Guid.NewGuid();
|
||||
var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
@@ -725,7 +725,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
CurrentRunId = currentRunId
|
||||
CurrentCycleId = currentCycleId
|
||||
});
|
||||
|
||||
// Add history entries for both movies in the current cycle
|
||||
@@ -735,7 +735,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 1,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-10),
|
||||
ItemTitle = "Movie 1"
|
||||
});
|
||||
@@ -744,7 +744,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 2,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-10),
|
||||
ItemTitle = "Movie 2"
|
||||
});
|
||||
@@ -777,14 +777,14 @@ public class SeekerTests : IDisposable
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert — search was triggered (new cycle started) and the RunId changed
|
||||
// Assert — search was triggered (new cycle started) and the CycleId changed
|
||||
mockArrClient.Verify(
|
||||
x => x.SearchItemsAsync(radarrInstance, It.IsAny<HashSet<SearchItem>>()),
|
||||
Times.Once);
|
||||
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == radarrInstance.Id);
|
||||
Assert.NotEqual(currentRunId, instanceConfig.CurrentRunId);
|
||||
Assert.NotEqual(currentCycleId, instanceConfig.CurrentCycleId);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -920,7 +920,7 @@ public class SeekerTests : IDisposable
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
|
||||
var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
var currentRunId = Guid.NewGuid();
|
||||
var currentCycleId = Guid.NewGuid();
|
||||
var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
@@ -928,7 +928,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
CurrentRunId = currentRunId,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 2
|
||||
});
|
||||
@@ -939,7 +939,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 1,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-2),
|
||||
ItemTitle = "Movie 1"
|
||||
});
|
||||
@@ -948,7 +948,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 2,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-1),
|
||||
ItemTitle = "Movie 2"
|
||||
});
|
||||
@@ -984,7 +984,7 @@ public class SeekerTests : IDisposable
|
||||
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == radarrInstance.Id);
|
||||
Assert.Equal(currentRunId, instanceConfig.CurrentRunId);
|
||||
Assert.Equal(currentCycleId, instanceConfig.CurrentCycleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -998,7 +998,7 @@ public class SeekerTests : IDisposable
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
|
||||
var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
var currentRunId = Guid.NewGuid();
|
||||
var currentCycleId = Guid.NewGuid();
|
||||
var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
@@ -1006,7 +1006,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
CurrentRunId = currentRunId,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 2
|
||||
});
|
||||
@@ -1017,7 +1017,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 1,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-10),
|
||||
ItemTitle = "Movie 1"
|
||||
});
|
||||
@@ -1026,7 +1026,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 2,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-8),
|
||||
ItemTitle = "Movie 2"
|
||||
});
|
||||
@@ -1066,7 +1066,7 @@ public class SeekerTests : IDisposable
|
||||
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == radarrInstance.Id);
|
||||
Assert.NotEqual(currentRunId, instanceConfig.CurrentRunId);
|
||||
Assert.NotEqual(currentCycleId, instanceConfig.CurrentCycleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1080,25 +1080,25 @@ public class SeekerTests : IDisposable
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
|
||||
var radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
var currentRunId = Guid.NewGuid();
|
||||
var currentCycleId = Guid.NewGuid();
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
CurrentRunId = currentRunId,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 30
|
||||
});
|
||||
|
||||
// History uses a DIFFERENT RunId — current cycle has no history entries
|
||||
var oldRunId = Guid.NewGuid();
|
||||
// History uses a DIFFERENT CycleId — current cycle has no history entries
|
||||
var oldCycleId = Guid.NewGuid();
|
||||
_fixture.DataContext.SeekerHistory.Add(new SeekerHistory
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 1,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = oldRunId,
|
||||
CycleId = oldCycleId,
|
||||
LastSearchedAt = DateTime.UtcNow.AddDays(-60),
|
||||
ItemTitle = "Movie 1"
|
||||
});
|
||||
@@ -1147,7 +1147,7 @@ public class SeekerTests : IDisposable
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
|
||||
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
var currentRunId = Guid.NewGuid();
|
||||
var currentCycleId = Guid.NewGuid();
|
||||
var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
@@ -1155,7 +1155,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ArrInstance = sonarrInstance,
|
||||
Enabled = true,
|
||||
CurrentRunId = currentRunId,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 1
|
||||
});
|
||||
@@ -1167,7 +1167,7 @@ public class SeekerTests : IDisposable
|
||||
ExternalItemId = 10,
|
||||
ItemType = InstanceType.Sonarr,
|
||||
SeasonNumber = 1,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-2),
|
||||
ItemTitle = "Test Series"
|
||||
});
|
||||
@@ -1210,7 +1210,7 @@ public class SeekerTests : IDisposable
|
||||
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == sonarrInstance.Id);
|
||||
Assert.Equal(currentRunId, instanceConfig.CurrentRunId);
|
||||
Assert.Equal(currentCycleId, instanceConfig.CurrentCycleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1224,7 +1224,7 @@ public class SeekerTests : IDisposable
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
|
||||
var sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
var currentRunId = Guid.NewGuid();
|
||||
var currentCycleId = Guid.NewGuid();
|
||||
var now = _fixture.TimeProvider.GetUtcNow().UtcDateTime;
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
@@ -1232,7 +1232,7 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ArrInstance = sonarrInstance,
|
||||
Enabled = true,
|
||||
CurrentRunId = currentRunId,
|
||||
CurrentCycleId = currentCycleId,
|
||||
MinCycleTimeDays = 7,
|
||||
TotalEligibleItems = 1
|
||||
});
|
||||
@@ -1244,7 +1244,7 @@ public class SeekerTests : IDisposable
|
||||
ExternalItemId = 10,
|
||||
ItemType = InstanceType.Sonarr,
|
||||
SeasonNumber = 1,
|
||||
RunId = currentRunId,
|
||||
CycleId = currentCycleId,
|
||||
LastSearchedAt = now.AddDays(-10),
|
||||
ItemTitle = "Test Series"
|
||||
});
|
||||
@@ -1291,7 +1291,7 @@ public class SeekerTests : IDisposable
|
||||
|
||||
var instanceConfig = await _fixture.DataContext.SeekerInstanceConfigs
|
||||
.FirstAsync(s => s.ArrInstanceId == sonarrInstance.Id);
|
||||
Assert.NotEqual(currentRunId, instanceConfig.CurrentRunId);
|
||||
Assert.NotEqual(currentCycleId, instanceConfig.CurrentCycleId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -1309,13 +1309,13 @@ public class SeekerTests : IDisposable
|
||||
|
||||
// Instance A: cycle complete, waiting for MinCycleTimeDays (oldest LastProcessedAt — would be picked first)
|
||||
var instanceA = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext, "http://radarr-a:7878");
|
||||
var runIdA = Guid.NewGuid();
|
||||
var cycleIdA = Guid.NewGuid();
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = instanceA.Id,
|
||||
ArrInstance = instanceA,
|
||||
Enabled = true,
|
||||
CurrentRunId = runIdA,
|
||||
CurrentCycleId = cycleIdA,
|
||||
MinCycleTimeDays = 30,
|
||||
TotalEligibleItems = 1,
|
||||
LastProcessedAt = now.AddDays(-5) // Oldest — round-robin would pick this first
|
||||
@@ -1325,20 +1325,20 @@ public class SeekerTests : IDisposable
|
||||
ArrInstanceId = instanceA.Id,
|
||||
ExternalItemId = 1,
|
||||
ItemType = InstanceType.Radarr,
|
||||
RunId = runIdA,
|
||||
CycleId = cycleIdA,
|
||||
LastSearchedAt = now.AddDays(-2), // Cycle started 2 days ago, MinCycleTimeDays=30
|
||||
ItemTitle = "Movie A"
|
||||
});
|
||||
|
||||
// Instance B: has work to do (newer LastProcessedAt)
|
||||
var instanceB = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext, "http://radarr-b:7878");
|
||||
var runIdB = Guid.NewGuid();
|
||||
var cycleIdB = Guid.NewGuid();
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = instanceB.Id,
|
||||
ArrInstance = instanceB,
|
||||
Enabled = true,
|
||||
CurrentRunId = runIdB,
|
||||
CurrentCycleId = cycleIdB,
|
||||
MinCycleTimeDays = 5,
|
||||
TotalEligibleItems = 1,
|
||||
LastProcessedAt = now.AddDays(-1)
|
||||
|
||||
@@ -231,7 +231,7 @@ public class EventPublisher : IEventPublisher
|
||||
/// Publishes a search triggered event with context data and notifications.
|
||||
/// Returns the event ID so the SeekerCommandMonitor can update it on completion.
|
||||
/// </summary>
|
||||
public async Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> items, SeekerSearchType searchType, Guid? cycleRunId = null)
|
||||
public async Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> items, SeekerSearchType searchType, Guid? cycleId = null)
|
||||
{
|
||||
var itemList = items as string[] ?? items.ToArray();
|
||||
var itemsDisplay = string.Join(", ", itemList.Take(5)) + (itemList.Length > 5 ? $" (+{itemList.Length - 5} more)" : "");
|
||||
@@ -242,7 +242,7 @@ public class EventPublisher : IEventPublisher
|
||||
Message = $"Searched {itemCount} items on {instanceName}: {itemsDisplay}",
|
||||
Severity = EventSeverity.Information,
|
||||
Data = JsonSerializer.Serialize(
|
||||
new { InstanceName = instanceName, ItemCount = itemCount, Items = itemList, SearchType = searchType.ToString(), CycleRunId = cycleRunId },
|
||||
new { InstanceName = instanceName, ItemCount = itemCount, Items = itemList, SearchType = searchType.ToString(), CycleId = cycleId },
|
||||
new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }),
|
||||
SearchStatus = SearchCommandStatus.Pending,
|
||||
JobRunId = ContextProvider.TryGetJobRunId(),
|
||||
@@ -250,7 +250,7 @@ public class EventPublisher : IEventPublisher
|
||||
InstanceUrl = (ContextProvider.Get(ContextProvider.Keys.ArrInstanceUrl) as Uri)?.ToString(),
|
||||
DownloadClientType = ContextProvider.Get(ContextProvider.Keys.DownloadClientType) is DownloadClientTypeName dct ? dct : null,
|
||||
DownloadClientName = ContextProvider.Get(ContextProvider.Keys.DownloadClientName) as string,
|
||||
CycleRunId = cycleRunId,
|
||||
CycleId = cycleId,
|
||||
};
|
||||
|
||||
eventEntity.IsDryRun = await _dryRunInterceptor.IsDryRunEnabled();
|
||||
|
||||
@@ -20,7 +20,7 @@ public interface IEventPublisher
|
||||
|
||||
Task PublishSearchNotTriggered(string hash, string itemName);
|
||||
|
||||
Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> items, SeekerSearchType searchType, Guid? cycleRunId = null);
|
||||
Task<Guid> PublishSearchTriggered(string instanceName, int itemCount, IEnumerable<string> items, SeekerSearchType searchType, Guid? cycleId = null);
|
||||
|
||||
Task PublishSearchCompleted(Guid eventId, SearchCommandStatus status, object? resultData = null);
|
||||
}
|
||||
@@ -315,7 +315,7 @@ public sealed class Seeker : IHandler
|
||||
// Load search history for the current cycle
|
||||
List<SeekerHistory> currentCycleHistory = await _dataContext.SeekerHistory
|
||||
.AsNoTracking()
|
||||
.Where(h => h.ArrInstanceId == arrInstance.Id && h.RunId == instanceConfig.CurrentRunId)
|
||||
.Where(h => h.ArrInstanceId == arrInstance.Id && h.CycleId == instanceConfig.CurrentCycleId)
|
||||
.ToListAsync();
|
||||
|
||||
// Load all history for stale cleanup
|
||||
@@ -379,13 +379,13 @@ public sealed class Seeker : IHandler
|
||||
List<long> commandIds = await arrClient.SearchItemsAsync(arrInstance, searchItems);
|
||||
|
||||
// Publish event (always saved, flagged with IsDryRun in EventPublisher)
|
||||
Guid eventId = await _eventPublisher.PublishSearchTriggered(arrInstance.Name, searchItems.Count, selectedNames, SeekerSearchType.Proactive, instanceConfig.CurrentRunId);
|
||||
Guid eventId = await _eventPublisher.PublishSearchTriggered(arrInstance.Name, searchItems.Count, selectedNames, SeekerSearchType.Proactive, instanceConfig.CurrentCycleId);
|
||||
|
||||
_logger.LogInformation("Searched {Count} items on {InstanceName}: {Items}",
|
||||
searchItems.Count, arrInstance.Name, string.Join(", ", selectedNames));
|
||||
|
||||
// Update search history (always, so stats are accurate during dry run)
|
||||
await UpdateSearchHistoryAsync(arrInstance.Id, instanceType, instanceConfig.CurrentRunId, historyIds, selectedNames, seasonNumber, isDryRun);
|
||||
await UpdateSearchHistoryAsync(arrInstance.Id, instanceType, instanceConfig.CurrentCycleId, historyIds, selectedNames, seasonNumber, isDryRun);
|
||||
|
||||
if (!isDryRun)
|
||||
{
|
||||
@@ -396,7 +396,7 @@ public sealed class Seeker : IHandler
|
||||
|
||||
// Cleanup stale history entries and old cycle history
|
||||
await CleanupStaleHistoryAsync(arrInstance.Id, instanceType, allLibraryIds, allHistoryExternalIds);
|
||||
await CleanupOldCycleHistoryAsync(arrInstance, instanceConfig.CurrentRunId);
|
||||
await CleanupOldCycleHistoryAsync(arrInstance, instanceConfig.CurrentCycleId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,7 +470,7 @@ public sealed class Seeker : IHandler
|
||||
|
||||
if (!isDryRun)
|
||||
{
|
||||
instanceConfig.CurrentRunId = Guid.NewGuid();
|
||||
instanceConfig.CurrentCycleId = Guid.NewGuid();
|
||||
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
}
|
||||
@@ -587,7 +587,7 @@ public sealed class Seeker : IHandler
|
||||
candidates.Count, arrInstance.Name);
|
||||
if (!isDryRun)
|
||||
{
|
||||
instanceConfig.CurrentRunId = Guid.NewGuid();
|
||||
instanceConfig.CurrentCycleId = Guid.NewGuid();
|
||||
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
}
|
||||
@@ -736,7 +736,7 @@ public sealed class Seeker : IHandler
|
||||
private async Task UpdateSearchHistoryAsync(
|
||||
Guid arrInstanceId,
|
||||
InstanceType instanceType,
|
||||
Guid runId,
|
||||
Guid cycleId,
|
||||
List<long> searchedIds,
|
||||
List<string>? itemTitles = null,
|
||||
int seasonNumber = 0,
|
||||
@@ -755,7 +755,7 @@ public sealed class Seeker : IHandler
|
||||
&& h.ExternalItemId == id
|
||||
&& h.ItemType == instanceType
|
||||
&& h.SeasonNumber == seasonNumber
|
||||
&& h.RunId == runId);
|
||||
&& h.CycleId == cycleId);
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
@@ -774,7 +774,7 @@ public sealed class Seeker : IHandler
|
||||
ExternalItemId = id,
|
||||
ItemType = instanceType,
|
||||
SeasonNumber = seasonNumber,
|
||||
RunId = runId,
|
||||
CycleId = cycleId,
|
||||
LastSearchedAt = now,
|
||||
ItemTitle = title,
|
||||
IsDryRun = isDryRun,
|
||||
@@ -850,13 +850,13 @@ public sealed class Seeker : IHandler
|
||||
/// Removes history entries from previous cycles that are older than 30 days.
|
||||
/// Recent cycle history is retained for statistics and history viewing.
|
||||
/// </summary>
|
||||
private async Task CleanupOldCycleHistoryAsync(ArrInstance arrInstance, Guid currentRunId)
|
||||
private async Task CleanupOldCycleHistoryAsync(ArrInstance arrInstance, Guid currentCycleId)
|
||||
{
|
||||
DateTime cutoff = _timeProvider.GetUtcNow().UtcDateTime.AddDays(-30);
|
||||
|
||||
int deleted = await _dataContext.SeekerHistory
|
||||
.Where(h => h.ArrInstanceId == arrInstance.Id
|
||||
&& h.RunId != currentRunId
|
||||
&& h.CycleId != currentCycleId
|
||||
&& h.LastSearchedAt < cutoff)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
@@ -877,7 +877,7 @@ public sealed class Seeker : IHandler
|
||||
// Count distinct items searched in current cycle
|
||||
int cycleItemsSearched = await _dataContext.SeekerHistory
|
||||
.AsNoTracking()
|
||||
.Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.RunId == ic.CurrentRunId)
|
||||
.Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.CycleId == ic.CurrentCycleId)
|
||||
.Select(h => h.ExternalItemId)
|
||||
.Distinct()
|
||||
.CountAsync();
|
||||
@@ -899,7 +899,7 @@ public sealed class Seeker : IHandler
|
||||
// Cycle is complete, but check if min time has elapsed
|
||||
DateTime? cycleStartedAt = await _dataContext.SeekerHistory
|
||||
.AsNoTracking()
|
||||
.Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.RunId == ic.CurrentRunId)
|
||||
.Where(h => h.ArrInstanceId == ic.ArrInstanceId && h.CycleId == ic.CurrentCycleId)
|
||||
.MinAsync(h => (DateTime?)h.LastSearchedAt);
|
||||
|
||||
if (ShouldWaitForMinCycleTime(ic, cycleStartedAt))
|
||||
|
||||
@@ -234,7 +234,7 @@ public class DataContext : DbContext
|
||||
.HasForeignKey(s => s.ArrInstanceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasIndex(s => new { s.ArrInstanceId, s.ExternalItemId, s.ItemType, s.SeasonNumber, s.RunId }).IsUnique();
|
||||
entity.HasIndex(s => new { s.ArrInstanceId, s.ExternalItemId, s.ItemType, s.SeasonNumber, s.CycleId }).IsUnique();
|
||||
|
||||
entity.Property(s => s.LastSearchedAt).HasConversion(new UtcDateTimeConverter());
|
||||
});
|
||||
|
||||
@@ -40,10 +40,10 @@ public sealed record SeekerInstanceConfig
|
||||
public DateTime? LastProcessedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The current cycle run ID. All searches in the same cycle share this ID.
|
||||
/// The current cycle ID. All searches in the same cycle share this ID.
|
||||
/// When all eligible items have been searched, a new ID is generated to start a fresh cycle.
|
||||
/// </summary>
|
||||
public Guid CurrentRunId { get; set; } = Guid.NewGuid();
|
||||
public Guid CurrentCycleId { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Total number of eligible items in the library for this instance.
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Cleanuparr.Persistence.Models.Events;
|
||||
[Index(nameof(JobRunId))]
|
||||
[Index(nameof(InstanceType))]
|
||||
[Index(nameof(DownloadClientType))]
|
||||
[Index(nameof(CycleRunId))]
|
||||
[Index(nameof(CycleId))]
|
||||
public class AppEvent : IEvent
|
||||
{
|
||||
[Key]
|
||||
@@ -86,9 +86,9 @@ public class AppEvent : IEvent
|
||||
public DateTime? CompletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Seeker cycle run ID associated with this event (only set for SearchTriggered events)
|
||||
/// The Seeker cycle ID associated with this event (only set for SearchTriggered events)
|
||||
/// </summary>
|
||||
public Guid? CycleRunId { get; set; }
|
||||
public Guid? CycleId { get; set; }
|
||||
|
||||
public bool IsDryRun { get; set; }
|
||||
}
|
||||
@@ -41,10 +41,10 @@ public sealed record SeekerHistory
|
||||
public int SeasonNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The run ID for cycle-based tracking. All searches in the same cycle share a RunId.
|
||||
/// When all items have been searched, a new RunId is generated to start a fresh cycle.
|
||||
/// The cycle ID. All searches in the same cycle share a CycleId.
|
||||
/// When all items have been searched, a new CycleId is generated to start a fresh cycle.
|
||||
/// </summary>
|
||||
public Guid RunId { get; set; }
|
||||
public Guid CycleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When this item was last searched
|
||||
|
||||
@@ -25,10 +25,10 @@ export class SearchStatsApi {
|
||||
);
|
||||
}
|
||||
|
||||
getEvents(page = 1, pageSize = 50, instanceId?: string, cycleRunId?: string): Observable<PaginatedResult<SearchEvent>> {
|
||||
getEvents(page = 1, pageSize = 50, instanceId?: string, cycleId?: string): Observable<PaginatedResult<SearchEvent>> {
|
||||
const params: Record<string, string | number> = { page, pageSize };
|
||||
if (instanceId) params['instanceId'] = instanceId;
|
||||
if (cycleRunId) params['cycleRunId'] = cycleRunId;
|
||||
if (cycleId) params['cycleId'] = cycleId;
|
||||
return this.http.get<PaginatedResult<SearchEvent>>('/api/seeker/search-stats/events', { params });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface InstanceSearchStat {
|
||||
totalSearchCount: number;
|
||||
lastSearchedAt: string | null;
|
||||
lastProcessedAt: string | null;
|
||||
currentRunId: string | null;
|
||||
currentCycleId: string | null;
|
||||
cycleItemsSearched: number;
|
||||
cycleItemsTotal: number;
|
||||
cycleStartedAt: string | null;
|
||||
@@ -51,6 +51,6 @@ export interface SearchEvent {
|
||||
searchStatus: string | null;
|
||||
completedAt: string | null;
|
||||
grabbedItems: unknown[] | null;
|
||||
cycleRunId: string | null;
|
||||
cycleId: string | null;
|
||||
isDryRun: boolean;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
</div>
|
||||
<div class="instance-card__stat">
|
||||
<span class="instance-card__stat-value instance-card__stat-value--small">
|
||||
{{ inst.currentRunId ? inst.currentRunId.substring(0, 8) : '—' }}
|
||||
{{ inst.currentCycleId ? inst.currentCycleId.substring(0, 8) : '—' }}
|
||||
</span>
|
||||
<app-tooltip text="Unique identifier for the current search cycle. Changes when all items have been searched">
|
||||
<span class="instance-card__stat-label">Cycle ID</span>
|
||||
@@ -150,8 +150,8 @@
|
||||
@if (event.isDryRun) {
|
||||
<app-badge severity="accent" size="sm">Dry Run</app-badge>
|
||||
}
|
||||
@if (event.cycleRunId) {
|
||||
<span class="list-row__cycle">{{ event.cycleRunId.substring(0, 8) }}</span>
|
||||
@if (event.cycleId) {
|
||||
<span class="list-row__cycle">{{ event.cycleId.substring(0, 8) }}</span>
|
||||
}
|
||||
<span class="list-row__meta">{{ event.instanceName }}</span>
|
||||
<span class="list-row__time">{{ event.timestamp | date:'yyyy-MM-dd HH:mm' }}</span>
|
||||
@@ -251,8 +251,8 @@
|
||||
@if (event.isDryRun) {
|
||||
<app-badge severity="accent" size="sm">Dry Run</app-badge>
|
||||
}
|
||||
@if (event.cycleRunId) {
|
||||
<span class="list-row__cycle">{{ event.cycleRunId.substring(0, 8) }}</span>
|
||||
@if (event.cycleId) {
|
||||
<span class="list-row__cycle">{{ event.cycleId.substring(0, 8) }}</span>
|
||||
}
|
||||
<span class="list-row__time">{{ event.timestamp | date:'yyyy-MM-dd HH:mm' }}</span>
|
||||
</div>
|
||||
|
||||
@@ -256,14 +256,14 @@ export class SearchStatsComponent implements OnInit {
|
||||
private loadEvents(): void {
|
||||
this.loading.set(true);
|
||||
const instanceId = this.selectedInstanceId() || undefined;
|
||||
let cycleRunId: string | undefined;
|
||||
let cycleId: string | undefined;
|
||||
|
||||
if (this.cycleFilter() === 'current' && instanceId) {
|
||||
const instance = this.summary()?.perInstanceStats.find(s => s.instanceId === instanceId);
|
||||
cycleRunId = instance?.currentRunId ?? undefined;
|
||||
cycleId = instance?.currentCycleId ?? undefined;
|
||||
}
|
||||
|
||||
this.api.getEvents(this.eventsPage(), this.pageSize(), instanceId, cycleRunId).subscribe({
|
||||
this.api.getEvents(this.eventsPage(), this.pageSize(), instanceId, cycleId).subscribe({
|
||||
next: (result) => {
|
||||
this.events.set(result.items);
|
||||
this.eventsTotalRecords.set(result.totalCount);
|
||||
|
||||
Reference in New Issue
Block a user