mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
fixed seeker searching cf upgrades on locked profiles
This commit is contained in:
1 parent
51b8776776
commit
edafb30282
3 files changed
+453
-7
No files matched your search
@@ -6,5 +6,7 @@ public sealed record ArrQualityProfile
|
||||
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public bool UpgradeAllowed { get; init; }
|
||||
|
||||
public int CutoffFormatScore { get; init; }
|
||||
}
|
||||
@@ -2567,6 +2567,10 @@ public class SeekerTests : IDisposable
|
||||
new SearchableMovie { Id = 3, Title = "Above Cutoff", Status = "released", Monitored = true, HasFile = true, MovieFile = new MovieFileInfo { Id = 300, QualityCutoffNotMet = false }, Tags = [] }
|
||||
]));
|
||||
|
||||
_radarrClient
|
||||
.GetQualityProfilesAsync(radarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = true }]);
|
||||
|
||||
SearchItem? capturedSearchItem = null;
|
||||
mockArrClient
|
||||
.SearchItemAsync(radarrInstance, Arg.Any<SearchItem>())
|
||||
@@ -2660,6 +2664,10 @@ public class SeekerTests : IDisposable
|
||||
new SearchableMovie { Id = 4, Title = "Both Met", Status = "released", Monitored = true, HasFile = true, MovieFile = new MovieFileInfo { Id = 400, QualityCutoffNotMet = false }, Tags = [] }
|
||||
]));
|
||||
|
||||
_radarrClient
|
||||
.GetQualityProfilesAsync(radarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = true }]);
|
||||
|
||||
List<SearchItem> capturedSearchItems = [];
|
||||
mockArrClient
|
||||
.SearchItemAsync(radarrInstance, Arg.Any<SearchItem>())
|
||||
@@ -2685,6 +2693,265 @@ public class SeekerTests : IDisposable
|
||||
capturedSearchItems.ShouldNotContain(item => item.Id == 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Radarr_UseCustomFormatScore_UpgradesDisabledProfile_ExcludesBelowCutoffMovie()
|
||||
{
|
||||
// Arrange
|
||||
SeekerConfig config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
UseCutoff = false,
|
||||
UseCustomFormatScore = true
|
||||
});
|
||||
|
||||
_fixture.EventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 2,
|
||||
ItemType = InstanceType.Radarr,
|
||||
CurrentScore = 10,
|
||||
CutoffScore = 50,
|
||||
Title = "Below Cutoff"
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
|
||||
_fixture.ArrQueueReader
|
||||
.ReadAllAsync(mockArrClient, Arg.Any<ArrInstance>())
|
||||
.Returns([]);
|
||||
|
||||
_radarrClient
|
||||
.StreamAllMoviesAsync(radarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
ToAsyncEnumerable<SearchableMovie>([
|
||||
new SearchableMovie { Id = 2, Title = "Below Cutoff", Status = "released", Monitored = true, HasFile = true, QualityProfileId = 0, MovieFile = new MovieFileInfo { Id = 200, QualityCutoffNotMet = false }, Tags = [] }
|
||||
]));
|
||||
|
||||
_radarrClient
|
||||
.GetQualityProfilesAsync(radarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = false }]);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
SeekerJob sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await mockArrClient.DidNotReceive()
|
||||
.SearchItemAsync(Arg.Any<ArrInstance>(), Arg.Any<SearchItem>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Radarr_UseCustomFormatScore_UpgradesDisabledProfile_SearchesOnlyMissingMovie()
|
||||
{
|
||||
// Arrange
|
||||
SeekerConfig config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
UseCutoff = false,
|
||||
UseCustomFormatScore = true
|
||||
});
|
||||
|
||||
_fixture.EventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ExternalItemId = 2,
|
||||
ItemType = InstanceType.Radarr,
|
||||
CurrentScore = 10,
|
||||
CutoffScore = 50,
|
||||
Title = "Below Cutoff"
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
|
||||
_fixture.ArrQueueReader
|
||||
.ReadAllAsync(mockArrClient, Arg.Any<ArrInstance>())
|
||||
.Returns([]);
|
||||
|
||||
_radarrClient
|
||||
.StreamAllMoviesAsync(radarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
ToAsyncEnumerable<SearchableMovie>([
|
||||
new SearchableMovie { Id = 1, Title = "Missing Movie", Status = "released", Monitored = true, HasFile = false, QualityProfileId = 0, Tags = [] },
|
||||
new SearchableMovie { Id = 2, Title = "Below Cutoff", Status = "released", Monitored = true, HasFile = true, QualityProfileId = 0, MovieFile = new MovieFileInfo { Id = 200, QualityCutoffNotMet = false }, Tags = [] }
|
||||
]));
|
||||
|
||||
_radarrClient
|
||||
.GetQualityProfilesAsync(radarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = false }]);
|
||||
|
||||
SearchItem? capturedSearchItem = null;
|
||||
mockArrClient
|
||||
.SearchItemAsync(radarrInstance, Arg.Any<SearchItem>())
|
||||
.Returns(ci =>
|
||||
{
|
||||
capturedSearchItem = ci.ArgAt<SearchItem>(1);
|
||||
return 100L;
|
||||
});
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
SeekerJob sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
capturedSearchItem.ShouldNotBeNull();
|
||||
capturedSearchItem.Id.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Radarr_UseCutoffAndUseCustomFormatScore_UpgradesDisabledProfile_StillSearchesCutoffNotMet()
|
||||
{
|
||||
// Arrange
|
||||
SeekerConfig config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
UseCutoff = true,
|
||||
UseCustomFormatScore = true
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
|
||||
_fixture.ArrQueueReader
|
||||
.ReadAllAsync(mockArrClient, Arg.Any<ArrInstance>())
|
||||
.Returns([]);
|
||||
|
||||
_radarrClient
|
||||
.StreamAllMoviesAsync(radarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
ToAsyncEnumerable<SearchableMovie>([
|
||||
new SearchableMovie { Id = 3, Title = "Cutoff Not Met", Status = "released", Monitored = true, HasFile = true, QualityProfileId = 0, MovieFile = new MovieFileInfo { Id = 300, QualityCutoffNotMet = true }, Tags = [] }
|
||||
]));
|
||||
|
||||
_radarrClient
|
||||
.GetQualityProfilesAsync(radarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = false }]);
|
||||
|
||||
mockArrClient
|
||||
.SearchItemAsync(radarrInstance, Arg.Any<SearchItem>())
|
||||
.Returns(100L);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
SeekerJob sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await _fixture.EventPublisher.Received(1)
|
||||
.PublishSearchTriggered(
|
||||
"Cutoff Not Met",
|
||||
SeekerSearchType.Proactive,
|
||||
SeekerSearchReason.QualityCutoffNotMet,
|
||||
Arg.Any<Guid?>(),
|
||||
Arg.Any<bool?>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Radarr_UseCustomFormatScoreDisabled_DoesNotFetchQualityProfiles()
|
||||
{
|
||||
// Arrange
|
||||
SeekerConfig config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ArrInstance = radarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
UseCutoff = false,
|
||||
UseCustomFormatScore = false
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
|
||||
_fixture.ArrQueueReader
|
||||
.ReadAllAsync(mockArrClient, Arg.Any<ArrInstance>())
|
||||
.Returns([]);
|
||||
|
||||
_radarrClient
|
||||
.StreamAllMoviesAsync(radarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
ToAsyncEnumerable<SearchableMovie>([
|
||||
new SearchableMovie { Id = 1, Title = "Missing Movie", Status = "released", Monitored = true, HasFile = false, Tags = [] }
|
||||
]));
|
||||
|
||||
mockArrClient
|
||||
.SearchItemAsync(radarrInstance, Arg.Any<SearchItem>())
|
||||
.Returns(100L);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
SeekerJob sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await _radarrClient.DidNotReceive()
|
||||
.GetQualityProfilesAsync(Arg.Any<ArrInstance>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Radarr_SearchReason_Missing()
|
||||
{
|
||||
@@ -3024,6 +3291,10 @@ public class SeekerTests : IDisposable
|
||||
new SearchableEpisode { Id = 101, SeasonNumber = 2, EpisodeNumber = 1, Monitored = true, AirDateUtc = pastDate, HasFile = true, EpisodeFileId = 600 }
|
||||
]);
|
||||
|
||||
_sonarrClient
|
||||
.GetQualityProfilesAsync(sonarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = true }]);
|
||||
|
||||
SeriesSearchItem? capturedSearchItem = null;
|
||||
mockArrClient
|
||||
.SearchItemAsync(Arg.Any<ArrInstance>(), Arg.Any<SearchItem>())
|
||||
@@ -3047,6 +3318,138 @@ public class SeekerTests : IDisposable
|
||||
capturedSearchItem.Id.ShouldBe(1); // Season 1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Sonarr_UseCustomFormatScore_UpgradesDisabledProfile_ExcludesBelowCutoffEpisode()
|
||||
{
|
||||
// Arrange
|
||||
SeekerConfig config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ArrInstance = sonarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
UseCutoff = false,
|
||||
UseCustomFormatScore = true
|
||||
});
|
||||
|
||||
_fixture.EventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry
|
||||
{
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ExternalItemId = 10,
|
||||
EpisodeId = 100,
|
||||
ItemType = InstanceType.Sonarr,
|
||||
CurrentScore = 10,
|
||||
CutoffScore = 50,
|
||||
Title = "Below CF Cutoff"
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
|
||||
_fixture.ArrQueueReader
|
||||
.ReadAllAsync(mockArrClient, Arg.Any<ArrInstance>())
|
||||
.Returns([]);
|
||||
|
||||
_sonarrClient
|
||||
.StreamAllSeriesAsync(sonarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
ToAsyncEnumerable<SearchableSeries>([
|
||||
new SearchableSeries { Id = 10, Title = "Test Series", Status = "continuing", Monitored = true, QualityProfileId = 0, Tags = [], Statistics = new SeriesStatistics { EpisodeCount = 10, EpisodeFileCount = 9 } }
|
||||
]));
|
||||
|
||||
DateTime pastDate = _fixture.TimeProvider.GetUtcNow().UtcDateTime.AddDays(-30);
|
||||
_sonarrClient
|
||||
.GetEpisodesAsync(Arg.Any<ArrInstance>(), 10)
|
||||
.Returns(
|
||||
[
|
||||
new SearchableEpisode { Id = 100, SeasonNumber = 1, EpisodeNumber = 1, Monitored = true, AirDateUtc = pastDate, HasFile = true, EpisodeFileId = 500 }
|
||||
]);
|
||||
|
||||
_sonarrClient
|
||||
.GetQualityProfilesAsync(sonarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = false }]);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
SeekerJob sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await _sonarrClient.Received().GetEpisodesAsync(Arg.Any<ArrInstance>(), 10);
|
||||
await mockArrClient.DidNotReceive()
|
||||
.SearchItemAsync(Arg.Any<ArrInstance>(), Arg.Any<SearchItem>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Sonarr_FullyDownloaded_UpgradesDisabledProfile_SkipsEpisodeFetch()
|
||||
{
|
||||
// Arrange
|
||||
SeekerConfig config = await _fixture.DataContext.SeekerConfigs.FirstAsync();
|
||||
config.SearchEnabled = true;
|
||||
config.ProactiveSearchEnabled = true;
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.DataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ArrInstance = sonarrInstance,
|
||||
Enabled = true,
|
||||
MonitoredOnly = false,
|
||||
UseCutoff = false,
|
||||
UseCustomFormatScore = true
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
|
||||
_fixture.ArrQueueReader
|
||||
.ReadAllAsync(mockArrClient, Arg.Any<ArrInstance>())
|
||||
.Returns([]);
|
||||
|
||||
_sonarrClient
|
||||
.StreamAllSeriesAsync(sonarrInstance, Arg.Any<CancellationToken>())
|
||||
.Returns(
|
||||
ToAsyncEnumerable<SearchableSeries>([
|
||||
new SearchableSeries { Id = 10, Title = "Fully Downloaded Series", Status = "continuing", Monitored = true, QualityProfileId = 0, Tags = [], Statistics = new SeriesStatistics { EpisodeCount = 10, EpisodeFileCount = 10 } }
|
||||
]));
|
||||
|
||||
_sonarrClient
|
||||
.GetQualityProfilesAsync(sonarrInstance)
|
||||
.Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = false }]);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
SeekerJob sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
await _sonarrClient.DidNotReceive()
|
||||
.GetEpisodesAsync(Arg.Any<ArrInstance>(), 10);
|
||||
await mockArrClient.DidNotReceive()
|
||||
.SearchItemAsync(Arg.Any<ArrInstance>(), Arg.Any<SearchItem>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_Sonarr_MonitoredOnlyTrue_ExcludesUnmonitoredSeries()
|
||||
{
|
||||
|
||||
@@ -534,12 +534,16 @@ public sealed class Seeker : IHandler
|
||||
|
||||
// Load cached CF scores when custom format score filtering is enabled
|
||||
Dictionary<long, CustomFormatScoreEntry>? cfScores = null;
|
||||
HashSet<int> upgradesDisabledProfileIds = [];
|
||||
if (instanceConfig.UseCustomFormatScore)
|
||||
{
|
||||
cfScores = await _eventsContext.CustomFormatScoreEntries
|
||||
.AsNoTracking()
|
||||
.Where(e => e.ArrInstanceId == arrInstance.Id && e.ItemType == InstanceType.Radarr)
|
||||
.ToDictionaryAsync(e => e.ExternalItemId);
|
||||
|
||||
List<ArrQualityProfile> profiles = await _radarrClient.GetQualityProfilesAsync(arrInstance);
|
||||
upgradesDisabledProfileIds = profiles.Where(p => !p.UpgradeAllowed).Select(p => p.Id).ToHashSet();
|
||||
}
|
||||
|
||||
DateTimeOffset graceCutoff = _timeProvider.GetUtcNow().AddHours(-config.PostReleaseGraceHours);
|
||||
@@ -573,11 +577,18 @@ public sealed class Seeker : IHandler
|
||||
continue;
|
||||
}
|
||||
|
||||
bool belowCustomFormatCutoff = instanceConfig.UseCustomFormatScore && cfScores != null && cfScores.TryGetValue(movie.Id, out var entry) && entry.CurrentScore < entry.CutoffScore;
|
||||
bool upgradesDisabled = belowCustomFormatCutoff && upgradesDisabledProfileIds.Contains(movie.QualityProfileId);
|
||||
bool passesQualityCheck = !movie.HasFile
|
||||
|| (instanceConfig.UseCutoff && (movie.MovieFile?.QualityCutoffNotMet ?? false))
|
||||
|| (instanceConfig.UseCustomFormatScore && cfScores != null && cfScores.TryGetValue(movie.Id, out var entry) && entry.CurrentScore < entry.CutoffScore);
|
||||
|| (belowCustomFormatCutoff && !upgradesDisabled);
|
||||
if (!passesQualityCheck)
|
||||
{
|
||||
if (upgradesDisabled)
|
||||
{
|
||||
_logger.LogDebug("skip | upgrades disabled in profile | {Title} | {InstanceName}", movie.Title, arrInstance.Name);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -699,6 +710,13 @@ public sealed class Seeker : IHandler
|
||||
Dictionary<long, string> tagsById = tags.ToDictionary(t => t.Id, t => t.Label);
|
||||
HashSet<string> skipTagSet = new(instanceConfig.SkipTags, StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
HashSet<int> upgradesDisabledProfileIds = [];
|
||||
if (instanceConfig.UseCustomFormatScore)
|
||||
{
|
||||
List<ArrQualityProfile> profiles = await _sonarrClient.GetQualityProfilesAsync(arrInstance);
|
||||
upgradesDisabledProfileIds = profiles.Where(p => !p.UpgradeAllowed).Select(p => p.Id).ToHashSet();
|
||||
}
|
||||
|
||||
DateTimeOffset graceCutoff = _timeProvider.GetUtcNow().AddHours(-config.PostReleaseGraceHours);
|
||||
|
||||
List<long> allLibraryIds = new();
|
||||
@@ -729,8 +747,15 @@ public sealed class Seeker : IHandler
|
||||
bool fullyDownloaded = series.Statistics is not null
|
||||
&& series.Statistics.EpisodeCount > 0
|
||||
&& series.Statistics.EpisodeFileCount >= series.Statistics.EpisodeCount;
|
||||
if (fullyDownloaded && !instanceConfig.UseCutoff && !instanceConfig.UseCustomFormatScore)
|
||||
bool customFormatUpgradesAllowed = instanceConfig.UseCustomFormatScore
|
||||
&& !upgradesDisabledProfileIds.Contains(series.QualityProfileId);
|
||||
if (fullyDownloaded && !instanceConfig.UseCutoff && !customFormatUpgradesAllowed)
|
||||
{
|
||||
if (instanceConfig.UseCustomFormatScore)
|
||||
{
|
||||
_logger.LogDebug("skip | upgrades disabled in profile | {SeriesTitle} | {InstanceName}", series.Title, arrInstance.Name);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -775,7 +800,8 @@ public sealed class Seeker : IHandler
|
||||
seriesTitle = series.Title;
|
||||
|
||||
(SeriesSearchItem? searchItem, SearchableEpisode? selectedEpisode, SeekerSearchReason searchReason) =
|
||||
await BuildSonarrSearchItemAsync(instanceConfig, arrInstance, seriesId, seriesHistory, seriesTitle, graceCutoff, cancellationToken, queuedSeasons);
|
||||
await BuildSonarrSearchItemAsync(instanceConfig, arrInstance, seriesId, seriesHistory, seriesTitle, graceCutoff,
|
||||
!upgradesDisabledProfileIds.Contains(series.QualityProfileId), cancellationToken, queuedSeasons);
|
||||
|
||||
if (searchItem is not null)
|
||||
{
|
||||
@@ -858,6 +884,7 @@ public sealed class Seeker : IHandler
|
||||
List<SeekerHistory> seriesHistory,
|
||||
string seriesTitle,
|
||||
DateTimeOffset graceCutoff,
|
||||
bool customFormatUpgradesAllowed,
|
||||
CancellationToken cancellationToken,
|
||||
HashSet<(long SeriesId, long SeasonNumber)>? queuedSeasons = null)
|
||||
{
|
||||
@@ -886,18 +913,33 @@ public sealed class Seeker : IHandler
|
||||
.ToDictionaryAsync(e => e.EpisodeId);
|
||||
}
|
||||
|
||||
bool BelowCustomFormatCutoff(SearchableEpisode e) =>
|
||||
cfScores != null && cfScores.TryGetValue(e.Id, out var entry) && entry.CurrentScore < entry.CutoffScore;
|
||||
|
||||
// Filter to qualifying episodes — UseCutoff and UseCustomFormatScore are OR-ed.
|
||||
// Cutoff status comes from the episodefile endpoint; items without a cached CF score are excluded.
|
||||
var qualifying = episodes
|
||||
List<SearchableEpisode> released = episodes
|
||||
.Where(e => e.AirDateUtc.HasValue && e.AirDateUtc.Value <= graceCutoff)
|
||||
.Where(e => !instanceConfig.MonitoredOnly || e.Monitored)
|
||||
.ToList();
|
||||
|
||||
var qualifying = released
|
||||
.Where(e => !e.HasFile
|
||||
|| (instanceConfig.UseCutoff && cutoffNotMetFileIds.Contains(e.EpisodeFileId))
|
||||
|| (instanceConfig.UseCustomFormatScore && cfScores != null && cfScores.TryGetValue(e.Id, out var entry) && entry.CurrentScore < entry.CutoffScore))
|
||||
|| (customFormatUpgradesAllowed && BelowCustomFormatCutoff(e)))
|
||||
.OrderBy(e => e.SeasonNumber)
|
||||
.ThenBy(e => e.EpisodeNumber)
|
||||
.ToList();
|
||||
|
||||
if (!customFormatUpgradesAllowed)
|
||||
{
|
||||
foreach (SearchableEpisode e in released.Where(e => BelowCustomFormatCutoff(e) && !qualifying.Contains(e)))
|
||||
{
|
||||
_logger.LogDebug("skip | upgrades disabled in profile | {SeriesTitle} S{Season:D2}E{Episode:D2} | {InstanceName}",
|
||||
seriesTitle, e.SeasonNumber, e.EpisodeNumber, arrInstance.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (qualifying.Count == 0)
|
||||
{
|
||||
return (null, null, default);
|
||||
@@ -949,8 +991,7 @@ public sealed class Seeker : IHandler
|
||||
var seasonEpisodes = qualifying.Where(e => e.SeasonNumber == selected.SeasonNumber).ToList();
|
||||
int missingCount = seasonEpisodes.Count(e => !e.HasFile);
|
||||
int cutoffCount = seasonEpisodes.Count(e => e.HasFile && cutoffNotMetFileIds.Contains(e.EpisodeFileId));
|
||||
int cfCount = seasonEpisodes.Count(e => e.HasFile && cfScores != null
|
||||
&& cfScores.TryGetValue(e.Id, out var cfEntry) && cfEntry.CurrentScore < cfEntry.CutoffScore);
|
||||
int cfCount = seasonEpisodes.Count(e => e.HasFile && customFormatUpgradesAllowed && BelowCustomFormatCutoff(e));
|
||||
|
||||
List<string> reasons = [];
|
||||
if (missingCount > 0)
|
||||
|
||||
Reference in new issue
Block a user