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(); }