added whisparr v2 release date to episodes

This commit is contained in:
Flaminel committed 2026-10-27 11:34:05 +02:00
1 parent b51d8323a6
commit 91dca8cb84
5 files changed
+96 -4

No files matched your search

@@ -14,6 +14,11 @@ public sealed record SearchableEpisode
public DateTimeOffset? AirDateUtc { get; init; }
/// <summary>
/// Whisparr v2 sends a release date in place of an air date.
/// </summary>
public DateOnly? ReleaseDate { get; init; }
public bool HasFile { get; init; }
public long EpisodeFileId { get; init; }
@@ -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<SearchableEpisode> 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<SearchableEpisode> 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<SearchableEpisode> 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<SearchableEpisode> 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, _) =>
@@ -1,5 +1,5 @@
namespace Cleanuparr.Infrastructure.Features.Arr.Interfaces;
public interface IWhisparrV2Client : IArrClient
public interface IWhisparrV2Client : ISonarrClient
{
}
@@ -267,7 +267,7 @@ public class SonarrClient : ArrClient, ISonarrClient
return await DeserializeStreamAsync<List<Tag>>(response) ?? [];
}
public async Task<List<SearchableEpisode>> GetEpisodesAsync(ArrInstance arrInstance, long seriesId, CancellationToken cancellationToken = default)
public virtual async Task<List<SearchableEpisode>> 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
}
/// <inheritdoc/>
public async Task<List<SearchableEpisode>> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
public virtual async Task<List<SearchableEpisode>> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync<SearchableEpisode>(arrInstance, "missing", monitored, cancellationToken);
/// <inheritdoc/>
public async Task<List<SearchableEpisode>> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
public virtual async Task<List<SearchableEpisode>> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
await GetAllWantedAsync<SearchableEpisode>(arrInstance, "cutoff", monitored, cancellationToken);
private async Task<List<Episode>?> GetEpisodesAsync(ArrInstance arrInstance, List<long> episodeIds)
@@ -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)
{
}
/// <inheritdoc/>
public override async Task<List<SearchableEpisode>> GetEpisodesAsync(ArrInstance arrInstance, long seriesId, CancellationToken cancellationToken = default) =>
WithAirDates(await base.GetEpisodesAsync(arrInstance, seriesId, cancellationToken));
/// <inheritdoc/>
public override async Task<List<SearchableEpisode>> GetMissingEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
WithAirDates(await base.GetMissingEpisodesAsync(arrInstance, monitored, cancellationToken));
/// <inheritdoc/>
public override async Task<List<SearchableEpisode>> GetCutoffUnmetEpisodesAsync(ArrInstance arrInstance, bool monitored, CancellationToken cancellationToken = default) =>
WithAirDates(await base.GetCutoffUnmetEpisodesAsync(arrInstance, monitored, cancellationToken));
private static List<SearchableEpisode> WithAirDates(List<SearchableEpisode> episodes) =>
episodes
.Select(e => e with
{
AirDateUtc = e.AirDateUtc
?? (e.ReleaseDate is { } releaseDate ? new DateTimeOffset(releaseDate.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero) : null),
})
.ToList();
}