diff --git a/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs b/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs
index 42614235..f5997085 100644
--- a/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs
+++ b/code/backend/Cleanuparr.Domain/Entities/Arr/SearchableEpisode.cs
@@ -14,6 +14,11 @@ public sealed record SearchableEpisode
public DateTimeOffset? AirDateUtc { get; init; }
+ ///
+ /// Whisparr v2 sends a release date in place of an air date.
+ ///
+ public DateOnly? ReleaseDate { get; init; }
+
public bool HasFile { get; init; }
public long EpisodeFileId { get; init; }
diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Arr/WhisparrV2ClientTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Arr/WhisparrV2ClientTests.cs
index d2b191af..73822bce 100644
--- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Arr/WhisparrV2ClientTests.cs
+++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Arr/WhisparrV2ClientTests.cs
@@ -135,8 +135,72 @@ public class WhisparrV2ClientTests
#endregion
+ #region Release date
+
+ [Fact]
+ public async Task GetEpisodesAsync_ReleaseDate_BecomesMidnightUtcAirDate()
+ {
+ // Arrange
+ RouteEpisodes("""[{"id":1,"seriesId":10,"seasonNumber":2024,"releaseDate":"2024-05-12"}]""");
+
+ // Act
+ List episodes = await _client.GetEpisodesAsync(_arrInstance, seriesId: 10);
+
+ // Assert
+ episodes.ShouldHaveSingleItem().AirDateUtc.ShouldBe(new DateTimeOffset(2024, 5, 12, 0, 0, 0, TimeSpan.Zero));
+ }
+
+ [Fact]
+ public async Task GetMissingEpisodesAsync_ReleaseDate_BecomesMidnightUtcAirDate()
+ {
+ // Arrange
+ RouteEpisodes("""{"page":1,"pageSize":1000,"totalRecords":1,"records":[{"id":1,"seriesId":10,"seasonNumber":2024,"releaseDate":"2024-05-12"}]}""");
+
+ // Act
+ List episodes = await _client.GetMissingEpisodesAsync(_arrInstance, monitored: true);
+
+ // Assert
+ episodes.ShouldHaveSingleItem().AirDateUtc.ShouldBe(new DateTimeOffset(2024, 5, 12, 0, 0, 0, TimeSpan.Zero));
+ }
+
+ [Fact]
+ public async Task GetCutoffUnmetEpisodesAsync_ReleaseDate_BecomesMidnightUtcAirDate()
+ {
+ // Arrange
+ RouteEpisodes("""{"page":1,"pageSize":1000,"totalRecords":1,"records":[{"id":1,"seriesId":10,"seasonNumber":2024,"releaseDate":"2024-05-12"}]}""");
+
+ // Act
+ List episodes = await _client.GetCutoffUnmetEpisodesAsync(_arrInstance, monitored: true);
+
+ // Assert
+ episodes.ShouldHaveSingleItem().AirDateUtc.ShouldBe(new DateTimeOffset(2024, 5, 12, 0, 0, 0, TimeSpan.Zero));
+ }
+
+ [Fact]
+ public async Task GetEpisodesAsync_NoReleaseDate_LeavesAirDateNull()
+ {
+ // Arrange
+ RouteEpisodes("""[{"id":1,"seriesId":10,"seasonNumber":2024}]""");
+
+ // Act
+ List episodes = await _client.GetEpisodesAsync(_arrInstance, seriesId: 10);
+
+ // Assert
+ episodes.ShouldHaveSingleItem().AirDateUtc.ShouldBeNull();
+ }
+
+ #endregion
+
#region Helpers
+ private void RouteEpisodes(string json)
+ {
+ _httpMessageHandler.SetupResponse((_, _) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(json, Encoding.UTF8, "application/json"),
+ }));
+ }
+
private void RouteResponses()
{
_httpMessageHandler.SetupResponse((req, _) =>
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IWhisparrV2Client.cs b/code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IWhisparrV2Client.cs
index 21755221..9607e92a 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IWhisparrV2Client.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/Arr/Interfaces/IWhisparrV2Client.cs
@@ -1,5 +1,5 @@
namespace Cleanuparr.Infrastructure.Features.Arr.Interfaces;
-public interface IWhisparrV2Client : IArrClient
+public interface IWhisparrV2Client : ISonarrClient
{
}
\ No newline at end of file
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Arr/SonarrClient.cs b/code/backend/Cleanuparr.Infrastructure/Features/Arr/SonarrClient.cs
index 6930d7e0..f490ecf2 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/Arr/SonarrClient.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/Arr/SonarrClient.cs
@@ -267,7 +267,7 @@ public class SonarrClient : ArrClient, ISonarrClient
return await DeserializeStreamAsync>(response) ?? [];
}
- public async Task> GetEpisodesAsync(ArrInstance arrInstance, long seriesId, CancellationToken cancellationToken = default)
+ public virtual async Task> GetEpisodesAsync(ArrInstance arrInstance, long seriesId, CancellationToken cancellationToken = default)
{
UriBuilder uriBuilder = new(arrInstance.Url);
uriBuilder.Path = $"{uriBuilder.Path.TrimEnd('/')}/api/{ApiVersion}/episode";
@@ -351,11 +351,11 @@ public class SonarrClient : ArrClient, ISonarrClient
}
///
- public async Task> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
+ public virtual async Task> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync(arrInstance, "missing", monitored, cancellationToken);
///
- public async Task> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
+ public virtual async Task> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync(arrInstance, "cutoff", monitored, cancellationToken);
private async Task?> GetEpisodesAsync(ArrInstance arrInstance, List episodeIds)
diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs b/code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs
index c53f300f..9f4c6c1c 100644
--- a/code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs
+++ b/code/backend/Cleanuparr.Infrastructure/Features/Arr/WhisparrV2Client.cs
@@ -1,6 +1,8 @@
+using Cleanuparr.Domain.Entities.Arr;
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
using Cleanuparr.Infrastructure.Features.ItemStriker;
using Cleanuparr.Infrastructure.Interceptors;
+using Cleanuparr.Persistence.Models.Configuration.Arr;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Features.Arr;
@@ -15,4 +17,25 @@ public class WhisparrV2Client : SonarrClient, IWhisparrV2Client
) : base(logger, httpClientFactory, striker, dryRunInterceptor)
{
}
+
+ ///
+ public override async Task> GetEpisodesAsync(ArrInstance arrInstance, long seriesId, CancellationToken cancellationToken = default) =>
+ WithAirDates(await base.GetEpisodesAsync(arrInstance, seriesId, cancellationToken));
+
+ ///
+ public override async Task> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
+ WithAirDates(await base.GetMissingEpisodesAsync(arrInstance, monitored, cancellationToken));
+
+ ///
+ public override async Task> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
+ WithAirDates(await base.GetCutoffUnmetEpisodesAsync(arrInstance, monitored, cancellationToken));
+
+ private static List WithAirDates(List episodes) =>
+ episodes
+ .Select(e => e with
+ {
+ AirDateUtc = e.AirDateUtc
+ ?? (e.ReleaseDate is { } releaseDate ? new DateTimeOffset(releaseDate.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero) : null),
+ })
+ .ToList();
}