mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
added radarr availability release gate to seeker
This commit is contained in:
1 parent
09458e974c
commit
469467249a
18 files changed
+4446
-10
No files matched your search
@@ -60,6 +60,7 @@ public class SeekerConfigControllerTests : IDisposable
|
||||
instance.MinCycleTimeDays.ShouldBe(7);
|
||||
instance.RecentGrabWindowHours.ShouldBe(24);
|
||||
instance.SonarrSearchMode.ShouldBe(SonarrSearchMode.Auto);
|
||||
instance.RadarrReleaseGate.ShouldBe(RadarrReleaseGate.ReleaseDate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -141,6 +142,25 @@ public class SeekerConfigControllerTests : IDisposable
|
||||
instance.RestMaxDays.ShouldBe(90);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSeekerConfig_WithRadarrReleaseGate_ReturnsStoredValue()
|
||||
{
|
||||
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
|
||||
_dataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarr.Id,
|
||||
Enabled = true,
|
||||
RadarrReleaseGate = RadarrReleaseGate.ArrAvailability
|
||||
});
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
IActionResult result = await _controller.GetSeekerConfig();
|
||||
OkObjectResult okResult = result.ShouldBeOfType<OkObjectResult>();
|
||||
SeekerConfigResponse response = okResult.Value.ShouldBeOfType<SeekerConfigResponse>();
|
||||
|
||||
response.Instances.ShouldHaveSingleItem().RadarrReleaseGate.ShouldBe(RadarrReleaseGate.ArrAvailability);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSeekerConfig_WithSonarrSearchMode_ReturnsStoredValue()
|
||||
{
|
||||
@@ -614,6 +634,40 @@ public class SeekerConfigControllerTests : IDisposable
|
||||
config.SonarrSearchMode.ShouldBe(SonarrSearchMode.Season);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateSeekerConfig_WithRadarrReleaseGate_OnExistingInstance_PersistsValue()
|
||||
{
|
||||
ArrInstance radarr = SeekerTestDataFactory.AddRadarrInstance(_dataContext);
|
||||
_dataContext.SeekerInstanceConfigs.Add(new SeekerInstanceConfig
|
||||
{
|
||||
ArrInstanceId = radarr.Id,
|
||||
Enabled = true,
|
||||
RadarrReleaseGate = RadarrReleaseGate.ReleaseDate
|
||||
});
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
UpdateSeekerConfigRequest request = new()
|
||||
{
|
||||
SearchEnabled = true,
|
||||
SearchInterval = 3,
|
||||
ProactiveSearchEnabled = true,
|
||||
Instances =
|
||||
[
|
||||
new UpdateSeekerInstanceConfigRequest
|
||||
{
|
||||
ArrInstanceId = radarr.Id,
|
||||
Enabled = true,
|
||||
RadarrReleaseGate = RadarrReleaseGate.ArrAvailability
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await _controller.UpdateSeekerConfig(request);
|
||||
|
||||
SeekerInstanceConfig config = await _dataContext.SeekerInstanceConfigs.SingleAsync();
|
||||
config.RadarrReleaseGate.ShouldBe(RadarrReleaseGate.ArrAvailability);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-1)]
|
||||
[InlineData(169)]
|
||||
|
||||
+2
@@ -28,6 +28,8 @@ public sealed record UpdateSeekerInstanceConfigRequest
|
||||
|
||||
public SonarrSearchMode SonarrSearchMode { get; init; } = SonarrSearchMode.Auto;
|
||||
|
||||
public RadarrReleaseGate RadarrReleaseGate { get; init; } = RadarrReleaseGate.ReleaseDate;
|
||||
|
||||
public bool RestUnitsEnabled { get; init; }
|
||||
|
||||
public int RestMaxDays { get; init; } = 30;
|
||||
|
||||
+2
@@ -36,6 +36,8 @@ public sealed record SeekerInstanceConfigResponse
|
||||
|
||||
public SonarrSearchMode SonarrSearchMode { get; init; }
|
||||
|
||||
public RadarrReleaseGate RadarrReleaseGate { get; init; }
|
||||
|
||||
public bool RestUnitsEnabled { get; init; }
|
||||
|
||||
public int RestMaxDays { get; init; }
|
||||
|
||||
@@ -72,6 +72,7 @@ public sealed class SeekerConfigController : ControllerBase
|
||||
UseCustomFormatScore = seekerConfig?.UseCustomFormatScore ?? false,
|
||||
RecentGrabWindowHours = seekerConfig?.RecentGrabWindowHours ?? 24,
|
||||
SonarrSearchMode = seekerConfig?.SonarrSearchMode ?? SonarrSearchMode.Auto,
|
||||
RadarrReleaseGate = seekerConfig?.RadarrReleaseGate ?? RadarrReleaseGate.ReleaseDate,
|
||||
RestUnitsEnabled = seekerConfig?.RestUnitsEnabled ?? false,
|
||||
RestMaxDays = seekerConfig?.RestMaxDays ?? 30,
|
||||
};
|
||||
@@ -143,6 +144,7 @@ public sealed class SeekerConfigController : ControllerBase
|
||||
existing.UseCustomFormatScore = instanceReq.UseCustomFormatScore;
|
||||
existing.RecentGrabWindowHours = instanceReq.RecentGrabWindowHours;
|
||||
existing.SonarrSearchMode = instanceReq.SonarrSearchMode;
|
||||
existing.RadarrReleaseGate = instanceReq.RadarrReleaseGate;
|
||||
existing.RestUnitsEnabled = instanceReq.RestUnitsEnabled;
|
||||
existing.RestMaxDays = instanceReq.RestMaxDays;
|
||||
existing.Validate();
|
||||
@@ -163,6 +165,7 @@ public sealed class SeekerConfigController : ControllerBase
|
||||
UseCustomFormatScore = instanceReq.UseCustomFormatScore,
|
||||
RecentGrabWindowHours = instanceReq.RecentGrabWindowHours,
|
||||
SonarrSearchMode = instanceReq.SonarrSearchMode,
|
||||
RadarrReleaseGate = instanceReq.RadarrReleaseGate,
|
||||
RestUnitsEnabled = instanceReq.RestUnitsEnabled,
|
||||
RestMaxDays = instanceReq.RestMaxDays,
|
||||
};
|
||||
|
||||
@@ -26,6 +26,11 @@ public sealed record SearchableMovie
|
||||
|
||||
public DateTimeOffset? InCinemas { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Radarr's own verdict from the movie's minimum availability and the availability delay.
|
||||
/// </summary>
|
||||
public bool IsAvailable { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Set when a search reached an indexer; RSS grabs leave it alone.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Cleanuparr.Domain.Enums;
|
||||
|
||||
public enum RadarrReleaseGate
|
||||
{
|
||||
/// <summary>
|
||||
/// Searches a movie once Radarr marks it released and its digital, physical or cinema date is past the grace period.
|
||||
/// </summary>
|
||||
ReleaseDate,
|
||||
|
||||
/// <summary>
|
||||
/// Searches a movie once Radarr counts it as available, using its minimum availability and availability delay.
|
||||
/// The grace period does not apply.
|
||||
/// </summary>
|
||||
ArrAvailability,
|
||||
}
|
||||
@@ -62,4 +62,9 @@ public enum SeekerExclusionReason
|
||||
/// Already searched in the current cycle
|
||||
/// </summary>
|
||||
SearchedThisCycle,
|
||||
|
||||
/// <summary>
|
||||
/// Radarr does not count the movie as available yet
|
||||
/// </summary>
|
||||
NotAvailable,
|
||||
}
|
||||
@@ -296,7 +296,7 @@ public class RadarrClientTests
|
||||
// Arrange
|
||||
_httpMessageHandler.SetupResponse((_, _) => Task.FromResult(JsonResponse(new[]
|
||||
{
|
||||
new SearchableMovie { Id = 1, Title = "Movie", QualityProfileId = 1 },
|
||||
new SearchableMovie { Id = 1, Title = "Movie", QualityProfileId = 1, IsAvailable = true },
|
||||
})));
|
||||
|
||||
// Act
|
||||
@@ -309,6 +309,7 @@ public class RadarrClientTests
|
||||
// Assert
|
||||
movies.Count.ShouldBe(1);
|
||||
movies[0].Title.ShouldBe("Movie");
|
||||
movies[0].IsAvailable.ShouldBeTrue();
|
||||
var request = _httpMessageHandler.CapturedRequests.ShouldHaveSingleItem();
|
||||
request.RequestUri!.AbsolutePath.ShouldBe("/api/v3/movie");
|
||||
}
|
||||
|
||||
+60
-1
@@ -81,7 +81,8 @@ public class SeekerUnitEvaluatorTests : IDisposable
|
||||
int qualityProfileId = 1,
|
||||
DateTimeOffset? digitalRelease = null,
|
||||
DateTimeOffset? added = null,
|
||||
DateTimeOffset? lastSearchTime = null)
|
||||
DateTimeOffset? lastSearchTime = null,
|
||||
bool isAvailable = false)
|
||||
{
|
||||
return new SearchableMovie
|
||||
{
|
||||
@@ -95,6 +96,7 @@ public class SeekerUnitEvaluatorTests : IDisposable
|
||||
DigitalRelease = digitalRelease,
|
||||
Added = added,
|
||||
LastSearchTime = lastSearchTime,
|
||||
IsAvailable = isAvailable,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -299,6 +301,63 @@ public class SeekerUnitEvaluatorTests : IDisposable
|
||||
ReasonOf(evaluation, "Movie 1").ShouldBe(SeekerExclusionReason.InGracePeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Radarr_ArrAvailability_InCinemasMovieThatIsAvailable_BuildsUnit()
|
||||
{
|
||||
// Arrange
|
||||
SetupRadarr([Movie(1, status: "inCinemas", isAvailable: true)]);
|
||||
|
||||
// Act
|
||||
SeekerEvaluation evaluation = await EvaluateRadarrAsync(
|
||||
InstanceConfig(_radarr, c => c.RadarrReleaseGate = RadarrReleaseGate.ArrAvailability));
|
||||
|
||||
// Assert
|
||||
evaluation.Units.ShouldHaveSingleItem().Name.ShouldBe("Movie 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Radarr_ArrAvailability_ReleasedMovieThatIsNotAvailable_ExcludesAsNotAvailable()
|
||||
{
|
||||
// Arrange
|
||||
SetupRadarr([Movie(1, digitalRelease: _timeProvider.GetUtcNow().AddDays(-30), isAvailable: false)]);
|
||||
|
||||
// Act
|
||||
SeekerEvaluation evaluation = await EvaluateRadarrAsync(
|
||||
InstanceConfig(_radarr, c => c.RadarrReleaseGate = RadarrReleaseGate.ArrAvailability));
|
||||
|
||||
// Assert
|
||||
ReasonOf(evaluation, "Movie 1").ShouldBe(SeekerExclusionReason.NotAvailable);
|
||||
evaluation.Units.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Radarr_ArrAvailability_AvailableMovieInsideGraceWindow_BuildsUnit()
|
||||
{
|
||||
// Arrange
|
||||
SetupRadarr([Movie(1, digitalRelease: _timeProvider.GetUtcNow().AddHours(-1), isAvailable: true)]);
|
||||
|
||||
// Act
|
||||
SeekerEvaluation evaluation = await EvaluateRadarrAsync(
|
||||
InstanceConfig(_radarr, c => c.RadarrReleaseGate = RadarrReleaseGate.ArrAvailability));
|
||||
|
||||
// Assert
|
||||
evaluation.Units.ShouldHaveSingleItem().Name.ShouldBe("Movie 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Radarr_ReleaseDate_InCinemasMovieThatIsAvailable_ExcludesAsNotReleased()
|
||||
{
|
||||
// Arrange
|
||||
SetupRadarr([Movie(1, status: "inCinemas", isAvailable: true)]);
|
||||
|
||||
// Act
|
||||
SeekerEvaluation evaluation = await EvaluateRadarrAsync();
|
||||
|
||||
// Assert
|
||||
ReasonOf(evaluation, "Movie 1").ShouldBe(SeekerExclusionReason.NotReleased);
|
||||
evaluation.Units.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_Radarr_UnmonitoredWithMonitoredOnly_ExcludesAsNotMonitored()
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ public class EnumFallbackParityTests
|
||||
["RTorrentSeedingRule.PrivacyType"] = TorrentPrivacyType.Public,
|
||||
["SeekerConfig.SelectionStrategy"] = SelectionStrategy.BalancedWeighted,
|
||||
["SeekerInstanceConfig.SonarrSearchMode"] = SonarrSearchMode.Auto,
|
||||
["SeekerInstanceConfig.RadarrReleaseGate"] = RadarrReleaseGate.ReleaseDate,
|
||||
["NtfyConfig.AuthenticationType"] = NtfyAuthenticationType.None,
|
||||
["NtfyConfig.Priority"] = NtfyPriority.Default,
|
||||
["PushoverConfig.Priority"] = PushoverPriority.Normal,
|
||||
|
||||
@@ -309,15 +309,10 @@ public sealed class SeekerUnitEvaluator
|
||||
{
|
||||
allLibraryIds.Add(movie.Id);
|
||||
|
||||
if (movie.Status is not "released")
|
||||
SeekerExclusionReason? releaseExclusion = GetReleaseExclusion(movie, instanceConfig.RadarrReleaseGate, graceCutoff);
|
||||
if (releaseExclusion is not null)
|
||||
{
|
||||
exclusions.Add(new SeekerExclusion(movie.Title, SeekerExclusionReason.NotReleased));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!IsMoviePastGracePeriod(movie, graceCutoff))
|
||||
{
|
||||
exclusions.Add(new SeekerExclusion(movie.Title, SeekerExclusionReason.InGracePeriod));
|
||||
exclusions.Add(new SeekerExclusion(movie.Title, releaseExclusion.Value));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -946,6 +941,27 @@ public sealed class SeekerUnitEvaluator
|
||||
return releaseDate is null || releaseDate.Value <= graceCutoff;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns why the release gate keeps the movie out, or null when it lets it through.
|
||||
/// </summary>
|
||||
private static SeekerExclusionReason? GetReleaseExclusion(
|
||||
SearchableMovie movie,
|
||||
RadarrReleaseGate gate,
|
||||
DateTimeOffset graceCutoff)
|
||||
{
|
||||
if (gate is RadarrReleaseGate.ArrAvailability)
|
||||
{
|
||||
return movie.IsAvailable ? null : SeekerExclusionReason.NotAvailable;
|
||||
}
|
||||
|
||||
if (movie.Status is not "released")
|
||||
{
|
||||
return SeekerExclusionReason.NotReleased;
|
||||
}
|
||||
|
||||
return IsMoviePastGracePeriod(movie, graceCutoff) ? null : SeekerExclusionReason.InGracePeriod;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns why the tags keep the item out, or null when they let it through. A skip tag wins over an include tag.
|
||||
/// </summary>
|
||||
|
||||
+2101
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 AddSeekerRadarrReleaseGate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "radarr_release_gate",
|
||||
schema: "data",
|
||||
table: "seeker_instance_configs",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "releasedate");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "radarr_release_gate",
|
||||
schema: "data",
|
||||
table: "seeker_instance_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -1747,6 +1747,11 @@ namespace Cleanuparr.Persistence.Postgres.Migrations.Data
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("monitored_only");
|
||||
|
||||
b.Property<string>("RadarrReleaseGate")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("radarr_release_gate");
|
||||
|
||||
b.Property<int>("RecentGrabWindowHours")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("recent_grab_window_hours");
|
||||
|
||||
+2097
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 AddSeekerRadarrReleaseGate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "radarr_release_gate",
|
||||
table: "seeker_instance_configs",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "releasedate");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "radarr_release_gate",
|
||||
table: "seeker_instance_configs");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1743,6 +1743,11 @@ namespace Cleanuparr.Persistence.Migrations.Data
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("monitored_only");
|
||||
|
||||
b.Property<string>("RadarrReleaseGate")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("radarr_release_gate");
|
||||
|
||||
b.Property<int>("RecentGrabWindowHours")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("recent_grab_window_hours");
|
||||
|
||||
+5
@@ -97,6 +97,11 @@ public sealed record SeekerInstanceConfig
|
||||
/// </summary>
|
||||
public SonarrSearchMode SonarrSearchMode { get; set; } = SonarrSearchMode.Auto;
|
||||
|
||||
/// <summary>
|
||||
/// How Radarr movies count as released. Ignored for Sonarr.
|
||||
/// </summary>
|
||||
public RadarrReleaseGate RadarrReleaseGate { get; set; } = RadarrReleaseGate.ReleaseDate;
|
||||
|
||||
/// <summary>
|
||||
/// Skips units whose recent searches in a row found nothing, for a rest that doubles with each further miss.
|
||||
/// </summary>
|
||||
|
||||
Reference in new issue
Block a user