changed seeker order to arr last search

This commit is contained in:
Flaminel committed 2026-10-22 12:52:50 +03:00
1 parent 0c9736493f
commit 41d8c28e93
5 files changed
+139 -4

No files matched your search

@@ -25,4 +25,9 @@ public sealed record SearchableMovie
public DateTimeOffset? PhysicalRelease { get; init; }
public DateTimeOffset? InCinemas { get; init; }
/// <summary>
/// Set when a search reached an indexer; RSS grabs leave it alone.
/// </summary>
public DateTimeOffset? LastSearchTime { get; init; }
}
@@ -80,7 +80,8 @@ public class SeekerUnitEvaluatorTests : IDisposable
bool hasFile = false,
int qualityProfileId = 1,
DateTimeOffset? digitalRelease = null,
DateTimeOffset? added = null)
DateTimeOffset? added = null,
DateTimeOffset? lastSearchTime = null)
{
return new SearchableMovie
{
@@ -93,6 +94,7 @@ public class SeekerUnitEvaluatorTests : IDisposable
QualityProfileId = qualityProfileId,
DigitalRelease = digitalRelease,
Added = added,
LastSearchTime = lastSearchTime,
};
}
@@ -133,7 +135,7 @@ public class SeekerUnitEvaluatorTests : IDisposable
};
}
private SearchableEpisode Episode(long id, long seriesId, int season = 1, int number = 1, DateTimeOffset? airDate = null, bool noAirDate = false)
private SearchableEpisode Episode(long id, long seriesId, int season = 1, int number = 1, DateTimeOffset? airDate = null, bool noAirDate = false, DateTimeOffset? lastSearchTime = null)
{
return new SearchableEpisode
{
@@ -143,6 +145,7 @@ public class SeekerUnitEvaluatorTests : IDisposable
EpisodeNumber = number,
Monitored = true,
AirDateUtc = noAirDate ? null : airDate ?? _timeProvider.GetUtcNow().AddDays(-30),
LastSearchTime = lastSearchTime,
};
}
@@ -1042,6 +1045,97 @@ public class SeekerUnitEvaluatorTests : IDisposable
candidates[1].SeasonNumber.ShouldBe(2);
}
[Fact]
public async Task SelectNext_OldestSearchFirst_OrdersMoviesByTheArrLastSearchTime()
{
// Arrange
DateTimeOffset now = _timeProvider.GetUtcNow();
SetupRadarr(
[
Movie(1, lastSearchTime: now.AddDays(-1)),
Movie(2),
Movie(3, lastSearchTime: now.AddDays(-5)),
]);
SeekerEvaluation evaluation = await EvaluateRadarrAsync();
// Act
List<SeekerSearchCandidate> candidates = SeekerUnitEvaluator.SelectNext(evaluation, SelectionStrategy.OldestSearchFirst, 3);
// Assert
candidates.Select(c => c.ItemId).ShouldBe([2L, 3L, 1L]);
}
[Fact]
public async Task SelectNext_OldestSearchFirst_RanksSeriesByTheirLatestEpisodeSearch()
{
// Arrange
DateTimeOffset now = _timeProvider.GetUtcNow();
SetupSonarr([Series(1), Series(2)],
[
Episode(10, 1, season: 1, lastSearchTime: now.AddDays(-10)),
Episode(11, 1, season: 2, lastSearchTime: now.AddDays(-1)),
Episode(20, 2, season: 1, lastSearchTime: now.AddDays(-5)),
]);
SeekerEvaluation evaluation = await EvaluateSonarrAsync();
// Act
List<SeekerSearchCandidate> candidates = SeekerUnitEvaluator.SelectNext(evaluation, SelectionStrategy.OldestSearchFirst, 1);
// Assert
candidates.ShouldHaveSingleItem().ItemId.ShouldBe(2);
}
[Fact]
public async Task SelectNext_OldestSearchFirst_CycleSearchNewerThanTheArrTimeWins()
{
// Arrange
DateTimeOffset now = _timeProvider.GetUtcNow();
SetupSonarr([Series(1), Series(2)],
[
Episode(10, 1, season: 1, lastSearchTime: now.AddDays(-10)),
Episode(11, 1, season: 2, lastSearchTime: now.AddDays(-10)),
Episode(20, 2, season: 1, lastSearchTime: now.AddDays(-5)),
]);
SeekerEvaluation evaluation = await EvaluateSonarrAsync(
history: [History(_sonarr, 1, now.AddDays(-1), seasonNumber: 1)]);
// Act
List<SeekerSearchCandidate> candidates = SeekerUnitEvaluator.SelectNext(evaluation, SelectionStrategy.OldestSearchFirst, 1);
// Assert
candidates.ShouldHaveSingleItem().ItemId.ShouldBe(2);
}
[Fact]
public async Task EvaluateAsync_Sonarr_CustomFormatOnlyEpisode_TakesTheCachedLastSearchTime()
{
// Arrange
DateTimeOffset lastSearchTime = _timeProvider.GetUtcNow().AddDays(-3);
SetupSonarr([Series(1)], []);
_sonarrClient.GetQualityProfilesAsync(Arg.Any<ArrInstance>()).Returns([]);
_eventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry
{
ArrInstanceId = _sonarr.Id,
ExternalItemId = 1,
EpisodeId = 20,
Title = "Series 1 S01E01",
ItemType = InstanceType.Sonarr,
SeasonNumber = 1,
AirDateUtc = _timeProvider.GetUtcNow().AddDays(-30),
LastSearchTime = lastSearchTime,
IsMonitored = true,
CurrentScore = 5,
CutoffScore = 10,
});
await _eventsContext.SaveChangesAsync();
// Act
SeekerEvaluation evaluation = await EvaluateSonarrAsync(InstanceConfig(_sonarr, c => c.UseCustomFormatScore = true));
// Assert
evaluation.Units.ShouldHaveSingleItem().LastSearchTime.ShouldBe(lastSearchTime);
}
#endregion
#region WithoutCycleHistory
@@ -1087,6 +1181,22 @@ public class SeekerUnitEvaluatorTests : IDisposable
fresh.LastSearchedByItem.ShouldBeEmpty();
}
[Fact]
public async Task WithoutCycleHistory_KeepsTheArrLastSearchTime()
{
// Arrange
DateTimeOffset lastSearchTime = _timeProvider.GetUtcNow().AddDays(-4);
SetupRadarr([Movie(1, lastSearchTime: lastSearchTime)]);
SeekerEvaluation evaluation = await EvaluateRadarrAsync(
history: [History(_radarr, 1, _timeProvider.GetUtcNow().AddDays(-1))]);
// Act
SeekerEvaluation fresh = evaluation.WithoutCycleHistory();
// Assert
fresh.Units.ShouldHaveSingleItem().LastSearchTime.ShouldBe(lastSearchTime);
}
[Fact]
public async Task WithoutCycleHistory_KeepsQueueBlocks()
{
@@ -41,6 +41,11 @@ public sealed record SeekerSearchUnit
/// </summary>
public DateTimeOffset? Added { get; init; }
/// <summary>
/// Latest search the arr recorded for the movie or any episode the unit covers.
/// </summary>
public DateTimeOffset? LastSearchTime { get; init; }
/// <summary>
/// Whether the current cycle already searched this unit.
/// </summary>
@@ -118,9 +118,17 @@ public sealed class SeekerUnitEvaluator
.GroupBy(u => u.ItemId)
.ToDictionary(g => g.Key, g => g.ToList());
Dictionary<long, DateTimeOffset?> arrSearchedByItem = evaluation.Units
.GroupBy(u => u.ItemId)
.ToDictionary(g => g.Key, g => g.Max(u => u.LastSearchTime));
List<(long Id, DateTimeOffset? Added, DateTimeOffset? LastSearched)> selectionCandidates = remainingByItem
.Select(p => (p.Key, p.Value[0].Added,
LastSearched: evaluation.LastSearchedByItem.TryGetValue(p.Key, out DateTimeOffset at) ? (DateTimeOffset?)at : null))
.Select(p =>
{
DateTimeOffset? cycleSearched = evaluation.LastSearchedByItem.TryGetValue(p.Key, out DateTimeOffset at) ? at : null;
DateTimeOffset? lastSearched = new[] { arrSearchedByItem[p.Key], cycleSearched }.Max();
return (p.Key, p.Value[0].Added, lastSearched);
})
.ToList();
return ItemSelectorFactory.Create(strategy)
@@ -294,6 +302,7 @@ public sealed class SeekerUnitEvaluator
Name = movie.Title,
Reasons = reasons,
Added = movie.Added,
LastSearchTime = movie.LastSearchTime,
Searched = false,
BlockedBy = blockedBy,
});
@@ -413,6 +422,7 @@ public sealed class SeekerUnitEvaluator
EpisodeId = e.Id,
EpisodeNumber = e.EpisodeNumber,
AirDateUtc = e.AirDateUtc,
LastSearchTime = e.LastSearchTime,
Monitored = e.Monitored,
Reason = reason,
};
@@ -475,6 +485,7 @@ public sealed class SeekerUnitEvaluator
EpisodeId = entry.EpisodeId,
Title = entry.Title,
AirDateUtc = entry.AirDateUtc,
LastSearchTime = entry.LastSearchTime,
Monitored = entry.IsMonitored,
Reason = SeekerSearchReason.CustomFormatScoreBelowCutoff,
});
@@ -593,6 +604,7 @@ public sealed class SeekerUnitEvaluator
ContentKind = kind,
Reasons = season.Select(w => w.Reason).ToList(),
Added = series.Added,
LastSearchTime = season.Max(w => w.LastSearchTime),
Searched = seasonSearched,
});
continue;
@@ -615,6 +627,7 @@ public sealed class SeekerUnitEvaluator
Reasons = episode.Select(w => w.Reason).ToList(),
AirDateUtc = first.AirDateUtc,
Added = series.Added,
LastSearchTime = episode.Max(w => w.LastSearchTime),
Searched = seasonSearched || searchedEpisodeIds.Contains(episode.Key),
});
}
@@ -25,6 +25,8 @@ internal sealed record SonarrWantedEpisode
public DateTimeOffset? AirDateUtc { get; init; }
public DateTimeOffset? LastSearchTime { get; init; }
public required bool Monitored { get; init; }
public required SeekerSearchReason Reason { get; init; }