fixed dry run usage

This commit is contained in:
Flaminel
2026-03-20 00:34:00 +02:00
parent f1533ad785
commit def4e8afda
7 changed files with 75 additions and 42 deletions

View File

@@ -15,4 +15,5 @@ public sealed record SearchEventResponse
public DateTime? CompletedAt { get; init; }
public object? GrabbedItems { get; init; }
public Guid? CycleRunId { get; init; }
public bool IsDryRun { get; init; }
}

View File

@@ -257,6 +257,7 @@ public sealed class SearchStatsController : ControllerBase
CompletedAt = e.CompletedAt,
GrabbedItems = parsed.GrabbedItems,
CycleRunId = e.CycleRunId,
IsDryRun = e.IsDryRun,
};
}).ToList();

View File

@@ -4,7 +4,6 @@ using Cleanuparr.Domain.Enums;
using Cleanuparr.Infrastructure.Events;
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
using Cleanuparr.Infrastructure.Features.Context;
using Cleanuparr.Infrastructure.Features.DownloadHunter.Models;
using Cleanuparr.Infrastructure.Features.DownloadRemover;
using Cleanuparr.Infrastructure.Features.DownloadRemover.Models;
using Cleanuparr.Infrastructure.Features.ItemStriker;

View File

@@ -253,7 +253,8 @@ public class EventPublisher : IEventPublisher
CycleRunId = cycleRunId,
};
await _dryRunInterceptor.InterceptAsync(SaveEventToDatabase, eventEntity);
eventEntity.IsDryRun = await _dryRunInterceptor.IsDryRunEnabled();
await SaveEventToDatabase(eventEntity);
await NotifyClientsAsync(eventEntity);
await _notificationPublisher.NotifySearchTriggered(instanceName, itemCount, itemList);

View File

@@ -55,6 +55,8 @@ public sealed class Seeker : IHandler
return;
}
bool isDryRun = await _dryRunInterceptor.IsDryRunEnabled();
// Replacement searches queued after download removal
SearchQueueItem? replacementItem = await _dataContext.SearchQueue
.OrderBy(q => q.CreatedAt)
@@ -62,7 +64,7 @@ public sealed class Seeker : IHandler
if (replacementItem is not null)
{
await ProcessReplacementItemAsync(replacementItem);
await ProcessReplacementItemAsync(replacementItem, isDryRun);
return;
}
@@ -72,10 +74,10 @@ public sealed class Seeker : IHandler
return;
}
await ProcessProactiveSearchAsync(config);
await ProcessProactiveSearchAsync(config, isDryRun);
}
private async Task ProcessReplacementItemAsync(SearchQueueItem item)
private async Task ProcessReplacementItemAsync(SearchQueueItem item, bool isDryRun)
{
ArrInstance? arrInstance = await _dataContext.ArrInstances
.Include(a => a.ArrConfig)
@@ -103,7 +105,10 @@ public sealed class Seeker : IHandler
Guid eventId = await _eventPublisher.PublishSearchTriggered(arrInstance.Name, 1, [item.Title], SeekerSearchType.Replacement);
await SaveCommandTrackersAsync(commandIds, eventId, arrInstance.Id, item.ArrInstance.ArrConfig.Type, item.ItemId, item.Title);
if (!isDryRun)
{
await SaveCommandTrackersAsync(commandIds, eventId, arrInstance.Id, item.ArrInstance.ArrConfig.Type, item.ItemId, item.Title);
}
_logger.LogInformation("Replacement search triggered for '{Title}' on {InstanceName}",
item.Title, arrInstance.Name);
@@ -115,8 +120,11 @@ public sealed class Seeker : IHandler
}
finally
{
_dataContext.SearchQueue.Remove(item);
await _dataContext.SaveChangesAsync();
if (!isDryRun)
{
_dataContext.SearchQueue.Remove(item);
await _dataContext.SaveChangesAsync();
}
}
}
@@ -138,7 +146,7 @@ public sealed class Seeker : IHandler
return [new SearchItem { Id = item.ItemId }];
}
private async Task ProcessProactiveSearchAsync(SeekerConfig config)
private async Task ProcessProactiveSearchAsync(SeekerConfig config, bool isDryRun)
{
List<SeekerInstanceConfig> instanceConfigs = await _dataContext.SeekerInstanceConfigs
.Include(s => s.ArrInstance)
@@ -163,19 +171,19 @@ public sealed class Seeker : IHandler
.OrderBy(s => s.LastProcessedAt ?? DateTime.MinValue)
.First();
await ProcessSingleInstanceAsync(config, nextInstance);
await ProcessSingleInstanceAsync(config, nextInstance, isDryRun);
}
else
{
// Process all enabled instances sequentially
foreach (SeekerInstanceConfig instanceConfig in instanceConfigs)
{
await ProcessSingleInstanceAsync(config, instanceConfig);
await ProcessSingleInstanceAsync(config, instanceConfig, isDryRun);
}
}
}
private async Task ProcessSingleInstanceAsync(SeekerConfig config, SeekerInstanceConfig instanceConfig)
private async Task ProcessSingleInstanceAsync(SeekerConfig config, SeekerInstanceConfig instanceConfig, bool isDryRun)
{
ArrInstance arrInstance = instanceConfig.ArrInstance;
InstanceType instanceType = arrInstance.ArrConfig.Type;
@@ -189,7 +197,7 @@ public sealed class Seeker : IHandler
try
{
await ProcessInstanceAsync(config, instanceConfig, arrInstance, instanceType);
await ProcessInstanceAsync(config, instanceConfig, arrInstance, instanceType, isDryRun);
}
catch (Exception ex)
{
@@ -197,17 +205,21 @@ public sealed class Seeker : IHandler
instanceType, arrInstance.Name);
}
// Always update LastProcessedAt so round-robin moves on
instanceConfig.LastProcessedAt = DateTime.UtcNow;
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
await _dataContext.SaveChangesAsync();
if (!isDryRun)
{
// Update LastProcessedAt so round-robin moves on
instanceConfig.LastProcessedAt = DateTime.UtcNow;
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
await _dataContext.SaveChangesAsync();
}
}
private async Task ProcessInstanceAsync(
SeekerConfig config,
SeekerInstanceConfig instanceConfig,
ArrInstance arrInstance,
InstanceType instanceType)
InstanceType instanceType,
bool isDryRun)
{
// Load search history for the current cycle
List<SeekerHistory> currentCycleHistory = await _dataContext.SeekerHistory
@@ -235,14 +247,14 @@ public sealed class Seeker : IHandler
if (instanceType == InstanceType.Radarr)
{
List<long> selectedIds;
(selectedIds, selectedNames, allLibraryIds) = await ProcessRadarrAsync(config, arrInstance, instanceConfig, itemSearchHistory);
(selectedIds, selectedNames, allLibraryIds) = await ProcessRadarrAsync(config, arrInstance, instanceConfig, itemSearchHistory, isDryRun);
searchItems = selectedIds.Select(id => new SearchItem { Id = id }).ToHashSet();
historyIds = selectedIds;
}
else
{
(searchItems, selectedNames, allLibraryIds, historyIds, seasonNumber) =
await ProcessSonarrAsync(config, arrInstance, instanceConfig, itemSearchHistory, currentCycleHistory);
await ProcessSonarrAsync(config, arrInstance, instanceConfig, itemSearchHistory, currentCycleHistory, isDryRun);
}
IEnumerable<long> historyExternalIds = allHistory.Select(h => h.ExternalItemId);
@@ -250,37 +262,45 @@ public sealed class Seeker : IHandler
if (searchItems.Count == 0)
{
_logger.LogDebug("No items selected for search on {InstanceName}", arrInstance.Name);
await CleanupStaleHistoryAsync(arrInstance.Id, instanceType, allLibraryIds, historyExternalIds);
if (!isDryRun)
{
await CleanupStaleHistoryAsync(arrInstance.Id, instanceType, allLibraryIds, historyExternalIds);
}
return;
}
// Trigger search
// Trigger search (arr client guards the HTTP request via dry run interceptor)
IArrClient arrClient = _arrClientFactory.GetClient(instanceType, arrInstance.Version);
List<long> commandIds = await arrClient.SearchItemsAsync(arrInstance, searchItems);
// Update search history
await UpdateSearchHistoryAsync(arrInstance.Id, instanceType, instanceConfig.CurrentRunId, historyIds, selectedNames, seasonNumber);
// Publish event and track commands
// Publish event (always saved, flagged with IsDryRun in EventPublisher)
Guid eventId = await _eventPublisher.PublishSearchTriggered(arrInstance.Name, searchItems.Count, selectedNames, SeekerSearchType.Proactive, instanceConfig.CurrentRunId);
long externalItemId = historyIds.FirstOrDefault();
string itemTitle = selectedNames.FirstOrDefault() ?? string.Empty;
await SaveCommandTrackersAsync(commandIds, eventId, arrInstance.Id, instanceType, externalItemId, itemTitle, seasonNumber);
_logger.LogInformation("Searched {Count} items on {InstanceName}: {Items}",
searchItems.Count, arrInstance.Name, string.Join(", ", selectedNames));
// Cleanup stale history entries and old cycle history
await CleanupStaleHistoryAsync(arrInstance.Id, instanceType, allLibraryIds, historyExternalIds);
await CleanupOldCycleHistoryAsync(arrInstance.Id, instanceConfig.CurrentRunId);
if (!isDryRun)
{
// Update search history
await UpdateSearchHistoryAsync(arrInstance.Id, instanceType, instanceConfig.CurrentRunId, historyIds, selectedNames, seasonNumber);
// Track commands
long externalItemId = historyIds.FirstOrDefault();
string itemTitle = selectedNames.FirstOrDefault() ?? string.Empty;
await SaveCommandTrackersAsync(commandIds, eventId, arrInstance.Id, instanceType, externalItemId, itemTitle, seasonNumber);
// Cleanup stale history entries and old cycle history
await CleanupStaleHistoryAsync(arrInstance.Id, instanceType, allLibraryIds, historyExternalIds);
await CleanupOldCycleHistoryAsync(arrInstance.Id, instanceConfig.CurrentRunId);
}
}
private async Task<(List<long> SelectedIds, List<string> SelectedNames, List<long> AllLibraryIds)> ProcessRadarrAsync(
SeekerConfig config,
ArrInstance arrInstance,
SeekerInstanceConfig instanceConfig,
Dictionary<long, DateTime> searchHistory)
Dictionary<long, DateTime> searchHistory,
bool isDryRun)
{
List<SearchableMovie> movies = await _radarrClient.GetAllMoviesAsync(arrInstance);
List<long> allLibraryIds = movies.Select(m => m.Id).ToList();
@@ -320,9 +340,12 @@ public sealed class Seeker : IHandler
{
_logger.LogInformation("All {Count} items on {InstanceName} searched in current cycle, starting new cycle",
candidates.Count, arrInstance.Name);
instanceConfig.CurrentRunId = Guid.NewGuid();
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
await _dataContext.SaveChangesAsync();
if (!isDryRun)
{
instanceConfig.CurrentRunId = Guid.NewGuid();
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
await _dataContext.SaveChangesAsync();
}
searchHistory = new Dictionary<long, DateTime>();
}
@@ -348,7 +371,8 @@ public sealed class Seeker : IHandler
ArrInstance arrInstance,
SeekerInstanceConfig instanceConfig,
Dictionary<long, DateTime> seriesSearchHistory,
List<SeekerHistory> currentCycleHistory)
List<SeekerHistory> currentCycleHistory,
bool isDryRun)
{
List<SearchableSeries> series = await _sonarrClient.GetAllSeriesAsync(arrInstance);
List<long> allLibraryIds = series.Select(s => s.Id).ToList();
@@ -415,13 +439,16 @@ public sealed class Seeker : IHandler
{
_logger.LogInformation("All series/seasons on {InstanceName} searched in current cycle, starting new cycle",
arrInstance.Name);
instanceConfig.CurrentRunId = Guid.NewGuid();
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
await _dataContext.SaveChangesAsync();
if (!isDryRun)
{
instanceConfig.CurrentRunId = Guid.NewGuid();
_dataContext.SeekerInstanceConfigs.Update(instanceConfig);
await _dataContext.SaveChangesAsync();
}
// Retry with fresh cycle
return await ProcessSonarrAsync(config, arrInstance, instanceConfig,
new Dictionary<long, DateTime>(), []);
new Dictionary<long, DateTime>(), [], isDryRun);
}
return ([], [], allLibraryIds, [], 0);

View File

@@ -51,4 +51,5 @@ export interface SearchEvent {
completedAt: string | null;
grabbedItems: unknown[] | null;
cycleRunId: string | null;
isDryRun: boolean;
}

View File

@@ -126,6 +126,9 @@
{{ event.searchStatus }}
</app-badge>
}
@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>
}