diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/CustomFormatScoreSyncerTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/CustomFormatScoreSyncerTests.cs index bfe085c9..291f4b45 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/CustomFormatScoreSyncerTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/CustomFormatScoreSyncerTests.cs @@ -452,10 +452,11 @@ public class CustomFormatScoreSyncerTests : IDisposable new SearchableSeries { Id = 10, Title = "Test Series", QualityProfileId = 1, Monitored = true } ])); + DateTimeOffset airDateUtc = new(2026, 1, 15, 0, 0, 0, TimeSpan.Zero); _sonarrClient .GetEpisodesAsync(sonarrInstance, 10) .Returns([ - new SearchableEpisode { Id = 100, SeasonNumber = 3, EpisodeNumber = 1, EpisodeFileId = 500, HasFile = true, Monitored = true } + new SearchableEpisode { Id = 100, SeasonNumber = 3, EpisodeNumber = 1, EpisodeFileId = 500, HasFile = true, Monitored = true, AirDateUtc = airDateUtc } ]); _sonarrClient @@ -469,10 +470,11 @@ public class CustomFormatScoreSyncerTests : IDisposable // Act await sut.ExecuteAsync(); - // Assert — the synced entry carries the episode's season number + // Assert: the synced entry carries the episode's season number and air date List entries = await _fixture.EventsContext.CustomFormatScoreEntries.ToListAsync(); entries.ShouldHaveSingleItem(); entries[0].SeasonNumber.ShouldBe(3); + entries[0].AirDateUtc.ShouldBe(airDateUtc); } [Fact] @@ -489,7 +491,7 @@ public class CustomFormatScoreSyncerTests : IDisposable UseCustomFormatScore = true }); - // Pre-existing entry from before SeasonNumber was tracked + // Pre-existing entry from before SeasonNumber/AirDateUtc were tracked _fixture.EventsContext.CustomFormatScoreEntries.Add(new CustomFormatScoreEntry { ArrInstanceId = sonarrInstance.Id, @@ -502,6 +504,7 @@ public class CustomFormatScoreSyncerTests : IDisposable CutoffScore = 500, QualityProfileName = "HD", SeasonNumber = null, + AirDateUtc = null, LastSyncedAt = DateTime.UtcNow.AddHours(-1) }); await _fixture.DataContext.SaveChangesAsync(); @@ -517,10 +520,11 @@ public class CustomFormatScoreSyncerTests : IDisposable new SearchableSeries { Id = 10, Title = "Test Series", QualityProfileId = 1, Monitored = true } ])); + DateTimeOffset airDateUtc = new(2026, 2, 20, 0, 0, 0, TimeSpan.Zero); _sonarrClient .GetEpisodesAsync(sonarrInstance, 10) .Returns([ - new SearchableEpisode { Id = 100, SeasonNumber = 2, EpisodeNumber = 1, EpisodeFileId = 500, HasFile = true, Monitored = true } + new SearchableEpisode { Id = 100, SeasonNumber = 2, EpisodeNumber = 1, EpisodeFileId = 500, HasFile = true, Monitored = true, AirDateUtc = airDateUtc } ]); _sonarrClient @@ -534,10 +538,11 @@ public class CustomFormatScoreSyncerTests : IDisposable // Act await sut.ExecuteAsync(); - // Assert — the next sync backfills the season number on the existing row + // Assert: the next sync backfills the season number and air date on the existing row List entries = await _fixture.EventsContext.CustomFormatScoreEntries.ToListAsync(); entries.ShouldHaveSingleItem(); entries[0].SeasonNumber.ShouldBe(2); + entries[0].AirDateUtc.ShouldBe(airDateUtc); } [Fact] diff --git a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs index fc85a151..14bfd3b0 100644 --- a/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs +++ b/code/backend/Cleanuparr.Infrastructure.Tests/Features/Jobs/SeekerTests.cs @@ -3532,6 +3532,7 @@ public class SeekerTests : IDisposable EpisodeId = 100, ItemType = InstanceType.Sonarr, SeasonNumber = 1, + AirDateUtc = _fixture.TimeProvider.GetUtcNow().AddDays(-30), CurrentScore = 10, CutoffScore = 50, Title = "Below CF Cutoff" @@ -3543,6 +3544,7 @@ public class SeekerTests : IDisposable EpisodeId = 101, ItemType = InstanceType.Sonarr, SeasonNumber = 2, + AirDateUtc = _fixture.TimeProvider.GetUtcNow().AddDays(-30), CurrentScore = 60, CutoffScore = 50, Title = "Above CF Cutoff" @@ -3619,6 +3621,7 @@ public class SeekerTests : IDisposable EpisodeId = 100, ItemType = InstanceType.Sonarr, SeasonNumber = 1, + AirDateUtc = _fixture.TimeProvider.GetUtcNow().AddDays(-30), CurrentScore = 10, CutoffScore = 50, Title = "Below CF Cutoff" @@ -4208,6 +4211,141 @@ public class SeekerTests : IDisposable .SearchItemAsync(Arg.Any(), Arg.Any()); } + [Fact] + public async Task ExecuteAsync_Sonarr_CustomFormatScoreEntryWithNullAirDate_IsNotACandidate() + { + // Arrange: a CF score row synced before the air date column existed carries no air date + var config = await _fixture.DataContext.SeekerConfigs.FirstAsync(); + config.SearchEnabled = true; + config.ProactiveSearchEnabled = true; + await _fixture.DataContext.SaveChangesAsync(); + await _fixture.EventsContext.SaveChangesAsync(); + + var 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, + SeasonNumber = 1, + AirDateUtc = null, + CurrentScore = 10, + CutoffScore = 50, + Title = "No Air Date Yet" + }); + await _fixture.DataContext.SaveChangesAsync(); + await _fixture.EventsContext.SaveChangesAsync(); + + var mockArrClient = Substitute.For(); + + _fixture.ArrQueueReader + .ReadAllAsync(mockArrClient, Arg.Any()) + .Returns([]); + + _sonarrClient + .StreamAllSeriesAsync(sonarrInstance, Arg.Any()) + .Returns(ToAsyncEnumerable([ + new SearchableSeries { Id = 10, Title = "Test Series", Status = "continuing", Monitored = true, Tags = [], Statistics = new SeriesStatistics { EpisodeCount = 10, EpisodeFileCount = 10 } } + ])); + + _sonarrClient + .GetQualityProfilesAsync(sonarrInstance) + .Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = true }]); + + _fixture.ArrClientFactory + .GetClient(InstanceType.Sonarr, Arg.Any()) + .Returns(mockArrClient); + + var sut = CreateSut(); + + // Act + await sut.ExecuteAsync(); + + // Assert: no air date to check against the grace period, so nothing was searched + await mockArrClient.DidNotReceive() + .SearchItemAsync(Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task ExecuteAsync_Sonarr_CustomFormatScoreEntryWithinGracePeriod_IsNotACandidate() + { + // Arrange: grace period of 24 hours, entry's episode aired 1 hour ago + var config = await _fixture.DataContext.SeekerConfigs.FirstAsync(); + config.SearchEnabled = true; + config.ProactiveSearchEnabled = true; + config.PostReleaseGraceHours = 24; + await _fixture.DataContext.SaveChangesAsync(); + await _fixture.EventsContext.SaveChangesAsync(); + + var 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, + SeasonNumber = 1, + AirDateUtc = _fixture.TimeProvider.GetUtcNow().AddHours(-1), + CurrentScore = 10, + CutoffScore = 50, + Title = "Recently Aired" + }); + await _fixture.DataContext.SaveChangesAsync(); + await _fixture.EventsContext.SaveChangesAsync(); + + var mockArrClient = Substitute.For(); + + _fixture.ArrQueueReader + .ReadAllAsync(mockArrClient, Arg.Any()) + .Returns([]); + + _sonarrClient + .StreamAllSeriesAsync(sonarrInstance, Arg.Any()) + .Returns(ToAsyncEnumerable([ + new SearchableSeries { Id = 10, Title = "Test Series", Status = "continuing", Monitored = true, Tags = [], Statistics = new SeriesStatistics { EpisodeCount = 10, EpisodeFileCount = 10 } } + ])); + + _sonarrClient + .GetQualityProfilesAsync(sonarrInstance) + .Returns([new ArrQualityProfile { Id = 0, UpgradeAllowed = true }]); + + _fixture.ArrClientFactory + .GetClient(InstanceType.Sonarr, Arg.Any()) + .Returns(mockArrClient); + + var sut = CreateSut(); + + // Act + await sut.ExecuteAsync(); + + // Assert: episode is within the grace period, so nothing was searched + await mockArrClient.DidNotReceive() + .SearchItemAsync(Arg.Any(), Arg.Any()); + } + [Fact] public async Task ExecuteAsync_Sonarr_CustomFormatScoreEntryWithSeasonNumber_SearchesThatSeasonWithCustomFormatReason() { @@ -4237,6 +4375,7 @@ public class SeekerTests : IDisposable EpisodeId = 100, ItemType = InstanceType.Sonarr, SeasonNumber = 3, + AirDateUtc = _fixture.TimeProvider.GetUtcNow().AddDays(-30), CurrentScore = 10, CutoffScore = 50, Title = "Below CF Cutoff" @@ -4321,6 +4460,7 @@ public class SeekerTests : IDisposable EpisodeId = 100, ItemType = InstanceType.Sonarr, SeasonNumber = 1, + AirDateUtc = _fixture.TimeProvider.GetUtcNow().AddDays(-30), CurrentScore = 10, CutoffScore = 50, Title = "Below CF Cutoff" diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs index 89bcf1b5..10982856 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/CustomFormatScoreSyncer.cs @@ -264,7 +264,7 @@ public sealed class CustomFormatScoreSyncer : IHandler string profileName = profile?.Name ?? "Unknown"; existingEntries.TryGetValue(movie.Id, out CustomFormatScoreEntry? existing); - UpsertCustomFormatScore(existing, arrInstance.Id, movie.Id, 0, InstanceType.Radarr, movie.Title, fileId, cfScore, cutoffScore, profileName, movie.Monitored, now, seasonNumber: null); + UpsertCustomFormatScore(existing, arrInstance.Id, movie.Id, 0, InstanceType.Radarr, movie.Title, fileId, cfScore, cutoffScore, profileName, movie.Monitored, now, seasonNumber: null, airDateUtc: null); synced++; } @@ -417,7 +417,7 @@ public sealed class CustomFormatScoreSyncer : IHandler string title = $"{series.Title} S{episode.SeasonNumber:D2}E{episode.EpisodeNumber:D2}"; existingEntries.TryGetValue((series.Id, episode.Id), out CustomFormatScoreEntry? existing); - UpsertCustomFormatScore(existing, arrInstance.Id, series.Id, episode.Id, InstanceType.Sonarr, title, fileId, cfScore, cutoffScore, profileName, isMonitored, now, episode.SeasonNumber); + UpsertCustomFormatScore(existing, arrInstance.Id, series.Id, episode.Id, InstanceType.Sonarr, title, fileId, cfScore, cutoffScore, profileName, isMonitored, now, episode.SeasonNumber, episode.AirDateUtc); synced++; } @@ -513,7 +513,8 @@ public sealed class CustomFormatScoreSyncer : IHandler string profileName, bool isMonitored, DateTimeOffset now, - int? seasonNumber) + int? seasonNumber, + DateTimeOffset? airDateUtc) { if (existing is not null) { @@ -545,6 +546,7 @@ public sealed class CustomFormatScoreSyncer : IHandler existing.IsMonitored = isMonitored; existing.LastSyncedAt = now; existing.SeasonNumber = seasonNumber; + existing.AirDateUtc = airDateUtc; } else { @@ -562,6 +564,7 @@ public sealed class CustomFormatScoreSyncer : IHandler IsMonitored = isMonitored, LastSyncedAt = now, SeasonNumber = seasonNumber, + AirDateUtc = airDateUtc, }); // Record initial score in history diff --git a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs index d31a10f0..ae12bf75 100644 --- a/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs +++ b/code/backend/Cleanuparr.Infrastructure/Features/Jobs/Seeker.cs @@ -838,12 +838,14 @@ public sealed class Seeker : IHandler if (instanceConfig.UseCustomFormatScore) { - // Rows synced before the season column existed carry no season until the next sync. + // Rows synced before the season/air date columns existed carry neither until the next sync. List belowCutoff = await _eventsContext.CustomFormatScoreEntries .AsNoTracking() .Where(e => e.ArrInstanceId == arrInstance.Id && e.ItemType == InstanceType.Sonarr && e.SeasonNumber != null + && e.AirDateUtc != null + && e.AirDateUtc <= graceCutoff && e.CurrentScore < e.CutoffScore && (!instanceConfig.MonitoredOnly || e.IsMonitored)) .ToListAsync(cancellationToken); diff --git a/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261017120323_AddCustomFormatScoreSeasonNumber.Designer.cs b/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261018093113_AddCustomFormatScoreSeasonAndAirDate.Designer.cs similarity index 98% rename from code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261017120323_AddCustomFormatScoreSeasonNumber.Designer.cs rename to code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261018093113_AddCustomFormatScoreSeasonAndAirDate.Designer.cs index bd261399..d4830b1b 100644 --- a/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261017120323_AddCustomFormatScoreSeasonNumber.Designer.cs +++ b/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261018093113_AddCustomFormatScoreSeasonAndAirDate.Designer.cs @@ -13,8 +13,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Cleanuparr.Persistence.Postgres.Migrations.Events { [DbContext(typeof(EventsContext))] - [Migration("20261017120323_AddCustomFormatScoreSeasonNumber")] - partial class AddCustomFormatScoreSeasonNumber + [Migration("20261018093113_AddCustomFormatScoreSeasonAndAirDate")] + partial class AddCustomFormatScoreSeasonAndAirDate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -304,6 +304,10 @@ namespace Cleanuparr.Persistence.Postgres.Migrations.Events .HasColumnType("uuid") .HasColumnName("id"); + b.Property("AirDateUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date_utc"); + b.Property("ArrInstanceId") .HasColumnType("uuid") .HasColumnName("arr_instance_id"); diff --git a/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261017120323_AddCustomFormatScoreSeasonNumber.cs b/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261018093113_AddCustomFormatScoreSeasonAndAirDate.cs similarity index 57% rename from code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261017120323_AddCustomFormatScoreSeasonNumber.cs rename to code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261018093113_AddCustomFormatScoreSeasonAndAirDate.cs index 58470269..a07b8654 100644 --- a/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261017120323_AddCustomFormatScoreSeasonNumber.cs +++ b/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/20261018093113_AddCustomFormatScoreSeasonAndAirDate.cs @@ -1,15 +1,23 @@ -using Microsoft.EntityFrameworkCore.Migrations; +using System; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Cleanuparr.Persistence.Postgres.Migrations.Events { /// - public partial class AddCustomFormatScoreSeasonNumber : Migration + public partial class AddCustomFormatScoreSeasonAndAirDate : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AddColumn( + name: "air_date_utc", + schema: "events", + table: "custom_format_score_entries", + type: "timestamp with time zone", + nullable: true); + migrationBuilder.AddColumn( name: "season_number", schema: "events", @@ -21,6 +29,11 @@ namespace Cleanuparr.Persistence.Postgres.Migrations.Events /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropColumn( + name: "air_date_utc", + schema: "events", + table: "custom_format_score_entries"); + migrationBuilder.DropColumn( name: "season_number", schema: "events", diff --git a/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/EventsContextModelSnapshot.cs b/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/EventsContextModelSnapshot.cs index ba97e279..3dd63a44 100644 --- a/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/EventsContextModelSnapshot.cs +++ b/code/backend/Cleanuparr.Persistence.Postgres/Migrations/Events/EventsContextModelSnapshot.cs @@ -301,6 +301,10 @@ namespace Cleanuparr.Persistence.Postgres.Migrations.Events .HasColumnType("uuid") .HasColumnName("id"); + b.Property("AirDateUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("air_date_utc"); + b.Property("ArrInstanceId") .HasColumnType("uuid") .HasColumnName("arr_instance_id"); diff --git a/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261017120316_AddCustomFormatScoreSeasonNumber.Designer.cs b/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261018093103_AddCustomFormatScoreSeasonAndAirDate.Designer.cs similarity index 99% rename from code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261017120316_AddCustomFormatScoreSeasonNumber.Designer.cs rename to code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261018093103_AddCustomFormatScoreSeasonAndAirDate.Designer.cs index 10d8884f..d183782b 100644 --- a/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261017120316_AddCustomFormatScoreSeasonNumber.Designer.cs +++ b/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261018093103_AddCustomFormatScoreSeasonAndAirDate.Designer.cs @@ -11,8 +11,8 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Cleanuparr.Persistence.Migrations.Events { [DbContext(typeof(EventsContext))] - [Migration("20261017120316_AddCustomFormatScoreSeasonNumber")] - partial class AddCustomFormatScoreSeasonNumber + [Migration("20261018093103_AddCustomFormatScoreSeasonAndAirDate")] + partial class AddCustomFormatScoreSeasonAndAirDate { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -299,6 +299,10 @@ namespace Cleanuparr.Persistence.Migrations.Events .HasColumnType("TEXT") .HasColumnName("id"); + b.Property("AirDateUtc") + .HasColumnType("TEXT") + .HasColumnName("air_date_utc"); + b.Property("ArrInstanceId") .HasColumnType("TEXT") .HasColumnName("arr_instance_id"); diff --git a/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261017120316_AddCustomFormatScoreSeasonNumber.cs b/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261018093103_AddCustomFormatScoreSeasonAndAirDate.cs similarity index 64% rename from code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261017120316_AddCustomFormatScoreSeasonNumber.cs rename to code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261018093103_AddCustomFormatScoreSeasonAndAirDate.cs index f922d2de..9342a521 100644 --- a/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261017120316_AddCustomFormatScoreSeasonNumber.cs +++ b/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/20261018093103_AddCustomFormatScoreSeasonAndAirDate.cs @@ -5,11 +5,17 @@ namespace Cleanuparr.Persistence.Migrations.Events { /// - public partial class AddCustomFormatScoreSeasonNumber : Migration + public partial class AddCustomFormatScoreSeasonAndAirDate : Migration { /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AddColumn( + name: "air_date_utc", + table: "custom_format_score_entries", + type: "TEXT", + nullable: true); + migrationBuilder.AddColumn( name: "season_number", table: "custom_format_score_entries", @@ -20,6 +26,10 @@ namespace Cleanuparr.Persistence.Migrations.Events /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropColumn( + name: "air_date_utc", + table: "custom_format_score_entries"); + migrationBuilder.DropColumn( name: "season_number", table: "custom_format_score_entries"); diff --git a/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/EventsContextModelSnapshot.cs b/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/EventsContextModelSnapshot.cs index 67f9d682..4ffbf793 100644 --- a/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/EventsContextModelSnapshot.cs +++ b/code/backend/Cleanuparr.Persistence.Sqlite/Migrations/Events/EventsContextModelSnapshot.cs @@ -296,6 +296,10 @@ namespace Cleanuparr.Persistence.Migrations.Events .HasColumnType("TEXT") .HasColumnName("id"); + b.Property("AirDateUtc") + .HasColumnType("TEXT") + .HasColumnName("air_date_utc"); + b.Property("ArrInstanceId") .HasColumnType("TEXT") .HasColumnName("arr_instance_id"); diff --git a/code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs b/code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs index fd7ef59e..cf38ceb4 100644 --- a/code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs +++ b/code/backend/Cleanuparr.Persistence/Models/State/CustomFormatScoreEntry.cs @@ -80,4 +80,9 @@ public sealed record CustomFormatScoreEntry /// Sonarr season of the episode; null for Radarr and for rows not yet re-synced. /// public int? SeasonNumber { get; set; } + + /// + /// Sonarr episode air date; null for Radarr and for rows not yet re-synced. + /// + public DateTimeOffset? AirDateUtc { get; set; } }