diff --git a/CHANGELOG.md b/CHANGELOG.md index 12f8d328a..2ad4cb04d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Fixed +- Fix block scheduler deleting the current hour's playout items, taking the channel offline until the next block + ## [26.8.1] - 2026-08-29 ### Security - Fix GHSA-h3r4-r2f2-qf59 (CVE-PENDING) diff --git a/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs b/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs index 275cf18ef..1d2f91c09 100644 --- a/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/BlockScheduling/BlockPlayoutBuilderTests.cs @@ -638,6 +638,126 @@ public class BlockPlayoutBuilderTests result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(9)); } } + + [Test] + [CancelAfter(10_000)] + public async Task RemoveBefore_Must_Not_Move_Into_The_Future(CancellationToken cancellationToken) + { + var collection = new SmartCollection { Id = 1, Query = "asdf" }; + + var block = new Block + { + Id = 1, + Name = "Test Block", + Minutes = 50, + Items = + [ + new BlockItem + { + Id = 1, + CollectionType = CollectionType.SmartCollection, + PlaybackOrder = PlaybackOrder.Chronological, + Index = 1, + SmartCollection = collection, + SmartCollectionId = collection.Id + } + ], + StopScheduling = BlockStopScheduling.BeforeDurationEnd + }; + + var template = new Template { Id = 1, Items = [] }; + + template.Items.Add( + new TemplateItem + { + Block = block, + BlockId = block.Id, + StartTime = TimeSpan.FromHours(8), + Template = template, + TemplateId = template.Id + }); + + var playoutTemplate = new PlayoutTemplate + { + Id = 1, + Index = 1, + Template = template, + TemplateId = template.Id, + DaysOfMonth = AlternateScheduleSelector.AllDaysOfMonth(), + DaysOfWeek = AlternateScheduleSelector.AllDaysOfWeek(), + MonthsOfYear = AlternateScheduleSelector.AllMonthsOfYear() + }; + + var playout = new Playout + { + Id = 1, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + Templates = [playoutTemplate], + Items = [], + PlayoutHistory = [] + }; + + DateTimeOffset midnight = DateTimeOffset.Now - DateTimeOffset.Now.TimeOfDay; + DateTimeOffset now = midnight.AddHours(9); + + var mediaItems = new List + { + new Movie + { + Id = 1, + MovieMetadata = [new MovieMetadata { ReleaseDate = DateTime.Today }], + MediaVersions = + [ + new MediaVersion + { + Duration = TimeSpan.FromMinutes(25), + MediaFiles = [new MediaFile { Path = "/fake/path/1" }] + } + ] + } + }; + + var collectionRepo = new FakeMediaCollectionRepository(Map((collection.Id, mediaItems))); + + IConfigElementRepository configRepo = Substitute.For(); + configRepo + .GetValue(Arg.Is(ConfigElementKey.PlayoutDaysToBuild), Arg.Any()) + .Returns(Some(1)); + + var builder = new BlockPlayoutBuilder( + configRepo, + collectionRepo, + Substitute.For(), + Substitute.For(), + Substitute.For(), + new LoggerFactory().CreateLogger()); + + var referenceData = new PlayoutReferenceData( + playout.Channel, + Option.None, + [], + playout.Templates.ToList(), + null, + [], + [], + TimeSpan.Zero); + + Either buildResult = await builder.Build( + now, + playout, + referenceData, + PlayoutBuildMode.Continue, + cancellationToken); + + buildResult.IsRight.ShouldBeTrue(); + foreach (PlayoutBuildResult result in buildResult.RightToSeq()) + { + DateTimeOffset removeBefore = result.RemoveBefore.IfNone(DateTimeOffset.MinValue); + await TestContext.Out.WriteLineAsync($"build start = {now:O}"); + await TestContext.Out.WriteLineAsync($"RemoveBefore = {removeBefore:O}"); + removeBefore.ShouldBeLessThanOrEqualTo(now); + } + } } diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index 5cfc811dc..427457fb3 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -75,7 +75,11 @@ public class BlockPlayoutBuilder( } // always start at the beginning of the block - start = blocksToSchedule.Min(b => b.Start); + DateTimeOffset firstBlockStart = blocksToSchedule.Min(b => b.Start); + if (firstBlockStart < start) + { + start = firstBlockStart; + } // get all collection items for the playout Map> collectionMediaItems =