using Destructurama; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling; using ErsatzTV.Core.Tests.Fakes; using FluentAssertions; using Microsoft.Extensions.Logging; using NSubstitute; using NUnit.Framework; using Serilog; namespace ErsatzTV.Core.Tests.Scheduling; [TestFixture] public class PlayoutBuilderTests { [SetUp] public void SetUp() => _cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; private readonly ILogger _logger; public PlayoutBuilderTests() { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() .Destructure.UsingAttributes() .CreateLogger(); ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); _logger = loggerFactory.CreateLogger(); } private CancellationToken _cancellationToken; [TestFixture] public class NewPlayout : PlayoutBuilderTests { [Test] public async Task OnlyZeroDurationItem_Should_Abort() { var mediaItems = new List { TestMovie(1, TimeSpan.Zero, DateTime.Today) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, _cancellationToken); result.Items.Should().BeEmpty(); } [Test] public async Task ZeroDurationItem_Should_BeSkipped() { var mediaItems = new List { TestMovie(1, TimeSpan.Zero, DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task OnlyFileNotFoundItem_Should_Abort() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today) }; mediaItems[0].State = MediaItemState.FileNotFound; IConfigElementRepository configRepo = Substitute.For(); configRepo .GetValue(Arg.Is(k => k.Key == ConfigElementKey.PlayoutSkipMissingItems.Key)) .Returns(Some(true)); (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, _cancellationToken); result.Items.Should().BeEmpty(); } [Test] public async Task FileNotFoundItem_Should_BeSkipped() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today) }; mediaItems[0].State = MediaItemState.FileNotFound; IConfigElementRepository configRepo = Substitute.For(); configRepo .GetValue(Arg.Is(k => k.Key == ConfigElementKey.PlayoutSkipMissingItems.Key)) .Returns(Some(true)); (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task OnlyUnavailableItem_Should_Abort() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today) }; mediaItems[0].State = MediaItemState.Unavailable; IConfigElementRepository configRepo = Substitute.For(); configRepo .GetValue(Arg.Is(k => k.Key == ConfigElementKey.PlayoutSkipMissingItems.Key)) .Returns(Some(true)); (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, _cancellationToken); result.Items.Should().BeEmpty(); } [Test] public async Task UnavailableItem_Should_BeSkipped() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today) }; mediaItems[0].State = MediaItemState.Unavailable; IConfigElementRepository configRepo = Substitute.For(); configRepo .GetValue(Arg.Is(k => k.Key == ConfigElementKey.PlayoutSkipMissingItems.Key)) .Returns(Some(true)); (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(2); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task FileNotFound_Should_NotBeSkippedIfConfigured() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; mediaItems[0].State = MediaItemState.FileNotFound; IConfigElementRepository configRepo = Substitute.For(); configRepo .GetValue(Arg.Is(k => k.Key == ConfigElementKey.PlayoutSkipMissingItems.Key)) .Returns(Some(false)); (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task Unavailable_Should_NotBeSkippedIfConfigured() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; mediaItems[0].State = MediaItemState.Unavailable; IConfigElementRepository configRepo = Substitute.For(); configRepo .GetValue(Arg.Is(k => k.Key == ConfigElementKey.PlayoutSkipMissingItems.Key)) .Returns(Some(false)); (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random, configRepo); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task InitialFlood_Should_StartAtMidnight() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items.Head().FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task InitialFlood_Should_StartAtMidnight_With_LateStart() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Random); DateTimeOffset start = HoursAfterMidnight(1); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(2); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(12)); } [Test] public async Task ChronologicalContent_Should_CreateChronologicalItems() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(4); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(4); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].MediaItemId.Should().Be(2); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(4)); } [Test] public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(1); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); Playout result2 = await builder.Build( playout, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); result2.Items.Count.Should().Be(2); result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result2.Items.Last().MediaItemId.Should().Be(2); result2.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(12)); result2.ProgramScheduleAnchors.Count.Should().Be(1); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); } [Test] public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(1); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); Playout result2 = await builder.Build( playout, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); result2.Items.Count.Should().Be(3); result2.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result2.Items[1].MediaItemId.Should().Be(2); result2.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result2.Items[2].MediaItemId.Should().Be(1); result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(18)); result2.ProgramScheduleAnchors.Count.Should().Be(1); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); } [Test] public async Task ShuffleFloodReset_Should_IgnoreAnchors() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(2)), TestMovie(4, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)), TestMovie(5, TimeSpan.FromHours(1), DateTime.Today.AddHours(4)), TestMovie(6, TimeSpan.FromHours(1), DateTime.Today.AddHours(5)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2, _cancellationToken); result2.Items.Count.Should().Be(6); result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); result2.ProgramScheduleAnchors.Count.Should().Be(1); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; firstSeedValue.Should().NotBe(secondSeedValue); } [Test] public async Task ContinuePlayout_ShuffleFlood_Should_MaintainRandomSeed() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Seed.Should().BeGreaterThan(0); int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); Playout result2 = await builder.Build( playout, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; firstSeedValue.Should().Be(secondSeedValue); result2.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task FloodContent_Should_FloodAroundFixedContent_One() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = null, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemOne { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(3), PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(5); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].MediaItemId.Should().Be(3); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[4].MediaItemId.Should().Be(2); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task FloodContent_Should_FloodAroundFixedContent_One_Multiple_Days() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = null, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemOne { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(3), PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(30); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(28); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].MediaItemId.Should().Be(3); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[4].MediaItemId.Should().Be(2); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[5].MediaItemId.Should().Be(1); result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); result.Items[6].MediaItemId.Should().Be(2); result.Items[7].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(8)); result.Items[7].MediaItemId.Should().Be(1); result.Items[8].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); result.Items[8].MediaItemId.Should().Be(2); result.Items[9].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); result.Items[9].MediaItemId.Should().Be(1); result.Items[10].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); result.Items[10].MediaItemId.Should().Be(2); result.Items[11].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[11].MediaItemId.Should().Be(1); result.Items[12].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(13)); result.Items[12].MediaItemId.Should().Be(2); result.Items[13].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(14)); result.Items[13].MediaItemId.Should().Be(1); result.Items[14].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(15)); result.Items[14].MediaItemId.Should().Be(2); result.Items[15].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(16)); result.Items[15].MediaItemId.Should().Be(1); result.Items[16].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(17)); result.Items[16].MediaItemId.Should().Be(2); result.Items[17].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[17].MediaItemId.Should().Be(1); result.Items[18].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(19)); result.Items[18].MediaItemId.Should().Be(2); result.Items[19].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(20)); result.Items[19].MediaItemId.Should().Be(1); result.Items[20].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(21)); result.Items[20].MediaItemId.Should().Be(2); result.Items[21].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(22)); result.Items[21].MediaItemId.Should().Be(1); result.Items[22].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(23)); result.Items[22].MediaItemId.Should().Be(2); result.Items[23].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[23].MediaItemId.Should().Be(1); result.Items[24].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[24].MediaItemId.Should().Be(2); result.Items[25].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[25].MediaItemId.Should().Be(1); result.Items[26].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[26].MediaItemId.Should().Be(3); result.Items[27].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[27].MediaItemId.Should().Be(2); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(30)); } [Test] public async Task FloodContent_Should_FloodAroundFixedContent_Multiple() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = null, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemMultiple { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(3), Count = 2, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(7); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].MediaItemId.Should().Be(3); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[4].MediaItemId.Should().Be(4); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[5].MediaItemId.Should().Be(2); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(7)); } [Test] public async Task FloodContent_Should_FloodWithFixedStartTime() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = TimeSpan.FromHours(7), PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemOne { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(12), PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(24); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(8)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); result.Items[3].MediaItemId.Should().Be(2); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); result.Items[4].MediaItemId.Should().Be(1); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[5].MediaItemId.Should().Be(3); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(31)); } [Test] public async Task ContinuePlayout_FloodContent_Should_FloodWithFixedStartTime_FromAnchor() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = TimeSpan.FromHours(7), PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemOne { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(12), PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(9).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, InFlood = true }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(32); Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken); result.Items.Count.Should().Be(5); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[3].MediaItemId.Should().Be(3); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); result.Items[4].MediaItemId.Should().Be(2); result.Anchor.InFlood.Should().BeTrue(); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(32)); } [Test] public async Task FloodContent_Should_FloodAroundFixedContent_DurationWithoutOfflineTail() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = null, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemDuration { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(2), PlayoutDuration = TimeSpan.FromHours(2), TailMode = TailMode.None, // immediately continue PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(7); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(3); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2.75)); result.Items[3].MediaItemId.Should().Be(1); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3.75)); result.Items[4].MediaItemId.Should().Be(2); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); result.Items[5].MediaItemId.Should().Be(1); result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5.75)); result.Items[6].MediaItemId.Should().Be(2); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6.75)); } [Test] public async Task MultipleContent_Should_WrapAroundDynamicContent_DurationWithoutOfflineTail() { var multipleCollection = new Collection { Id = 1, Name = "Multiple Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var dynamicCollection = new Collection { Id = 2, Name = "Dynamic Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(0.75), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1.5), new DateTime(2020, 1, 2)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (multipleCollection.Id, multipleCollection.MediaItems.ToList()), (dynamicCollection.Id, dynamicCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemMultiple { Id = 1, Index = 1, Collection = multipleCollection, CollectionId = multipleCollection.Id, StartTime = null, Count = 2, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemDuration { Id = 2, Index = 2, Collection = dynamicCollection, CollectionId = dynamicCollection.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(2), TailMode = TailMode.None, // immediately continue PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(3); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2.75)); result.Items[3].MediaItemId.Should().Be(1); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3.75)); result.Items[4].MediaItemId.Should().Be(2); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4.75)); result.Items[5].MediaItemId.Should().Be(4); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6.25)); } [Test] public async Task ContinuePlayout_Alternating_MultipleContent_Should_Maintain_Counts() { var collectionOne = new Collection { Id = 1, Name = "Multiple Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var collectionTwo = new Collection { Id = 2, Name = "Multiple Items 2", MediaItems = new List { TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemMultiple { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, Count = 3, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemMultiple { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, Count = 3, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(1).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, MultipleRemaining = 2 }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken); result.Items.Count.Should().Be(4); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[1].MediaItemId.Should().Be(1); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[2].MediaItemId.Should().Be(2); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[3].MediaItemId.Should().Be(2); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.MultipleRemaining.Should().Be(1); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] public async Task Auto_Zero_MultipleCount() { var collectionOne = new Collection { Id = 1, Name = "Multiple Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(3, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var collectionTwo = new Collection { Id = 2, Name = "Multiple Items 2", MediaItems = new List { TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(5, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemMultiple { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, Count = 0, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemMultiple { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, Count = 0, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(5); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(3); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].MediaItemId.Should().Be(4); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[4].MediaItemId.Should().Be(5); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.MultipleRemaining.Should().BeNull(); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] public async Task ContinuePlayout_Alternating_Duration_Should_Maintain_Duration() { var collectionOne = new Collection { Id = 1, Name = "Duration Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var collectionTwo = new Collection { Id = 2, Name = "Duration Items 2", MediaItems = new List { TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemDuration { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), TailMode = TailMode.None, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemDuration { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), TailMode = TailMode.None, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(1).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, DurationFinish = HoursAfterMidnight(3).UtcDateTime }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken); result.Items.Count.Should().Be(4); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[1].MediaItemId.Should().Be(1); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[2].MediaItemId.Should().Be(2); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[3].MediaItemId.Should().Be(2); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] public async Task Alternating_Duration_With_Filler_Should_Alternate_Schedule_Items() { var collectionOne = new Collection { Id = 1, Name = "Duration Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1)) } }; var collectionTwo = new Collection { Id = 2, Name = "Duration Items 2", MediaItems = new List { TestMovie(2, TimeSpan.FromMinutes(55), new DateTime(2020, 1, 1)) } }; var collectionThree = new Collection { Id = 3, Name = "Filler Items", MediaItems = new List { TestMovie(3, TimeSpan.FromMinutes(5), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()), (collectionThree.Id, collectionThree.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemDuration { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), PlaybackOrder = PlaybackOrder.Chronological, TailMode = TailMode.Filler, TailFiller = new FillerPreset { FillerKind = FillerKind.Tail, Collection = collectionThree, CollectionId = collectionThree.Id } }, new ProgramScheduleItemDuration { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), PlaybackOrder = PlaybackOrder.Chronological, TailMode = TailMode.Filler, TailFiller = new FillerPreset { FillerKind = FillerKind.Tail, Collection = collectionThree, CollectionId = collectionThree.Id } } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(12); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(55)); result.Items[1].MediaItemId.Should().Be(1); result.Items[2].StartOffset.TimeOfDay.Should().Be(new TimeSpan(1, 50, 0)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 45, 0)); result.Items[3].MediaItemId.Should().Be(3); result.Items[4].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 50, 0)); result.Items[4].MediaItemId.Should().Be(3); result.Items[5].StartOffset.TimeOfDay.Should().Be(new TimeSpan(2, 55, 0)); result.Items[5].MediaItemId.Should().Be(3); result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[6].MediaItemId.Should().Be(2); result.Items[7].StartOffset.TimeOfDay.Should().Be(new TimeSpan(3, 55, 0)); result.Items[7].MediaItemId.Should().Be(2); result.Items[8].StartOffset.TimeOfDay.Should().Be(new TimeSpan(4, 50, 0)); result.Items[8].MediaItemId.Should().Be(2); result.Items[9].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 45, 0)); result.Items[9].MediaItemId.Should().Be(3); result.Items[10].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 50, 0)); result.Items[10].MediaItemId.Should().Be(3); result.Items[11].StartOffset.TimeOfDay.Should().Be(new TimeSpan(5, 55, 0)); result.Items[11].MediaItemId.Should().Be(3); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.DurationFinish.Should().BeNull(); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task Multiple_With_Filler_Should_Keep_Filler_After_End_Of_Playout() { var collectionOne = new Collection { Id = 1, Name = "Duration Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromMinutes(61), new DateTime(2020, 1, 1)) } }; var collectionTwo = new Collection { Id = 2, Name = "Filler Items", MediaItems = new List { TestMovie(2, TimeSpan.FromMinutes(4), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemMultiple { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, Count = 1, PlaybackOrder = PlaybackOrder.Chronological, PostRollFiller = new FillerPreset { FillerKind = FillerKind.PostRoll, Collection = collectionTwo, CollectionId = collectionTwo.Id, FillerMode = FillerMode.Count, Count = 1 } } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(1); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(2); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(0)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromMinutes(61)); result.Items[1].MediaItemId.Should().Be(2); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.DurationFinish.Should().BeNull(); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddMinutes(65)); } [Test] public async Task Duration_Should_Skip_Items_That_Are_Too_Long() { var collectionOne = new Collection { Id = 1, Name = "Duration Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemDuration { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(1), PlaybackOrder = PlaybackOrder.Chronological, TailMode = TailMode.None } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); result.Items[0].MediaItemId.Should().Be(2); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[1].MediaItemId.Should().Be(4); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[2].MediaItemId.Should().Be(2); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[3].MediaItemId.Should().Be(4); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[4].MediaItemId.Should().Be(2); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(5)); result.Items[5].MediaItemId.Should().Be(4); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(0); result.Anchor.DurationFinish.Should().BeNull(); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(6)); } [Test] public async Task Two_Day_Playout_Should_Create_Date_Anchors_For_Midnight() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), new DateTime(2002, 1, 1)), TestMovie(2, TimeSpan.FromHours(6), new DateTime(2003, 1, 1)), TestMovie(3, TimeSpan.FromHours(6), new DateTime(2004, 1, 1)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromDays(2); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(8); result.Items[0].MediaItemId.Should().Be(1); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[1].MediaItemId.Should().Be(2); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[2].MediaItemId.Should().Be(3); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[2].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[3].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[3].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[4].MediaItemId.Should().Be(2); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[4].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[5].MediaItemId.Should().Be(3); result.Items[5].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[5].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[6].MediaItemId.Should().Be(1); result.Items[6].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[6].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[7].MediaItemId.Should().Be(2); result.Items[7].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[7].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.ProgramScheduleAnchors.Count.Should().Be(2); result.ProgramScheduleAnchors.Count(a => a.EnumeratorState.Index == 4 % 3).Should().Be(1); result.ProgramScheduleAnchors.Count(a => a.EnumeratorState.Index == 8 % 3).Should().Be(1); int seed = result.ProgramScheduleAnchors[0].EnumeratorState.Seed; result.ProgramScheduleAnchors.All(a => a.EnumeratorState.Seed == seed).Should().BeTrue(); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddDays(2)); } } [TestFixture] public class ResetPlayout : PlayoutBuilderTests { [Test] public async Task ShuffleFlood_Should_IgnoreAnchors() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(2)), TestMovie(4, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)), TestMovie(5, TimeSpan.FromHours(1), DateTime.Today.AddHours(4)), TestMovie(6, TimeSpan.FromHours(1), DateTime.Today.AddHours(5)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); Playout result2 = await builder.Build(playout, PlayoutBuildMode.Reset, start2, finish2, _cancellationToken); result2.Items.Count.Should().Be(6); result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); result2.ProgramScheduleAnchors.Count.Should().Be(1); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; firstSeedValue.Should().NotBe(secondSeedValue); } } [TestFixture] public class RefreshPlayout : PlayoutBuilderTests { [Test] public async Task Two_Day_Playout_Should_Refresh_From_Midnight_Anchor() { var collectionOne = new Collection { Id = 1, Name = "Duration Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), new DateTime(2002, 1, 1)), TestMovie(2, TimeSpan.FromHours(6), new DateTime(2003, 1, 1)), TestMovie(3, TimeSpan.FromHours(6), new DateTime(2004, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, // this should be ignored Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(1).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, DurationFinish = HoursAfterMidnight(3).UtcDateTime }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; playout.ProgramScheduleAnchors.Add( new PlayoutProgramScheduleAnchor { AnchorDate = HoursAfterMidnight(24).UtcDateTime, Collection = collectionOne, CollectionId = collectionOne.Id, CollectionType = ProgramScheduleItemCollectionType.Collection, EnumeratorState = new CollectionEnumeratorState { Index = 1, Seed = 12345 }, Playout = playout }); IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(24); DateTimeOffset finish = start + TimeSpan.FromDays(1); Playout result = await builder.Build(playout, PlayoutBuildMode.Refresh, start, finish, _cancellationToken); result.Items.Count.Should().Be(4); result.Items[0].MediaItemId.Should().Be(2); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Items[0].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[1].MediaItemId.Should().Be(3); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result.Items[1].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[2].MediaItemId.Should().Be(1); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[2].FinishOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[3].MediaItemId.Should().Be(2); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); result.Items[3].FinishOffset.TimeOfDay.Should().Be(TimeSpan.Zero); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(48)); } } [TestFixture] public class ContinuePlayout : PlayoutBuilderTests { [Test] public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(1); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); Playout result2 = await builder.Build( playout, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); result2.Items.Count.Should().Be(2); result2.Items.Last().StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result2.Items.Last().MediaItemId.Should().Be(2); result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(12)); result2.ProgramScheduleAnchors.Count.Should().Be(1); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); } [Test] public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(1); result.Items.Head().MediaItemId.Should().Be(1); result.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(6)); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); Playout result2 = await builder.Build( playout, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); result2.Items.Count.Should().Be(3); result2.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); result2.Items[1].MediaItemId.Should().Be(2); result2.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result2.Items[2].MediaItemId.Should().Be(1); result2.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(18)); result2.ProgramScheduleAnchors.Count.Should().Be(1); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); } [Test] public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems_MultiDay() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromDays(1); await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); playout.Items.Count.Should().Be(4); playout.Items.Map(i => i.MediaItemId).ToList().Should().Equal(1, 2, 1, 2); playout.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddDays(1)); playout.ProgramScheduleAnchors.Count.Should().Be(1); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); PlayoutProgramScheduleAnchor headAnchor = playout.ProgramScheduleAnchors.Head(); // throw in a detractor anchor - playout builder should prioritize the "continue" anchor playout.ProgramScheduleAnchors.Insert( 0, new PlayoutProgramScheduleAnchor { Id = headAnchor.Id + 1, Collection = headAnchor.Collection, CollectionId = headAnchor.CollectionId, Playout = playout, PlayoutId = playout.Id, AnchorDate = DateTime.Today.ToUniversalTime(), CollectionType = headAnchor.CollectionType, EnumeratorState = new CollectionEnumeratorState { Index = headAnchor.EnumeratorState.Index + 1, Seed = headAnchor.EnumeratorState.Seed }, MediaItem = headAnchor.MediaItem, MediaItemId = headAnchor.MediaItemId, MultiCollection = headAnchor.MultiCollection, MultiCollectionId = headAnchor.MultiCollectionId, SmartCollection = headAnchor.SmartCollection, SmartCollectionId = headAnchor.SmartCollectionId }); // continue 1h later DateTimeOffset start2 = HoursAfterMidnight(1); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1); await builder.Build(playout, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); playout.Items.Count.Should().Be(5); playout.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); playout.Items[0].MediaItemId.Should().Be(1); playout.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); playout.Items[1].MediaItemId.Should().Be(2); playout.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); playout.Items[2].MediaItemId.Should().Be(1); playout.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); playout.Items[3].MediaItemId.Should().Be(2); playout.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); playout.Items[4].MediaItemId.Should().Be(1); playout.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(30)); playout.ProgramScheduleAnchors.Count.Should().Be(2); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); // continue 1h later DateTimeOffset start3 = HoursAfterMidnight(2); DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1); await builder.Build(playout, PlayoutBuildMode.Continue, start3, finish3, _cancellationToken); playout.Items.Count.Should().Be(5); playout.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); playout.Items[0].MediaItemId.Should().Be(1); playout.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(6)); playout.Items[1].MediaItemId.Should().Be(2); playout.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); playout.Items[2].MediaItemId.Should().Be(1); playout.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(18)); playout.Items[3].MediaItemId.Should().Be(2); playout.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(0)); playout.Items[4].MediaItemId.Should().Be(1); playout.Anchor.NextStartOffset.Should().Be(DateTime.Today.AddHours(30)); playout.ProgramScheduleAnchors.Count.Should().Be(2); playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(1); } [Test] public async Task ShuffleFlood_Should_MaintainRandomSeed() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.ProgramScheduleAnchors.Count.Should().Be(1); result.ProgramScheduleAnchors.Head().EnumeratorState.Seed.Should().BeGreaterThan(0); result.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); int firstSeedValue = result.ProgramScheduleAnchors.Head().EnumeratorState.Seed; DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); Playout result2 = await builder.Build( result, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); int secondSeedValue = result2.ProgramScheduleAnchors.Head().EnumeratorState.Seed; firstSeedValue.Should().Be(secondSeedValue); result2.ProgramScheduleAnchors.Head().EnumeratorState.Index.Should().Be(0); } [Test] public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays() { var mediaItems = new List(); for (var i = 1; i <= 25; i++) { mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); } (PlayoutBuilder builder, Playout playout) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset finish = start + TimeSpan.FromDays(2); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(53); result.ProgramScheduleAnchors.Count.Should().Be(2); result.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).Should().BeTrue(); PlayoutProgramScheduleAnchor lastCheckpoint = result.ProgramScheduleAnchors .OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue) .First(); lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0); lastCheckpoint.EnumeratorState.Index.Should().Be(3); // we need to mess up the ordering to trigger the problematic behavior // this simulates the way the rows are loaded with EF PlayoutProgramScheduleAnchor oldest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate) .Last(); PlayoutProgramScheduleAnchor newest = result.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate) .First(); result.ProgramScheduleAnchors = new List { oldest, newest }; int firstSeedValue = lastCheckpoint.EnumeratorState.Seed; DateTimeOffset start2 = start.AddHours(1); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); Playout result2 = await builder.Build( result, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); PlayoutProgramScheduleAnchor continueAnchor = result2.ProgramScheduleAnchors.First(x => x.AnchorDate is null); int secondSeedValue = continueAnchor.EnumeratorState.Seed; // the continue anchor should have the same seed as the most recent (last) checkpoint from the first run firstSeedValue.Should().Be(secondSeedValue); } [Test] public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed() { var mediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) }; (PlayoutBuilder builder, Playout playout) = TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(6); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(6); result.ProgramScheduleAnchors.Count.Should().Be(2); PlayoutProgramScheduleAnchor primaryAnchor = result.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); primaryAnchor.EnumeratorState.Seed.Should().BeGreaterThan(0); primaryAnchor.EnumeratorState.Index.Should().Be(0); int firstSeedValue = primaryAnchor.EnumeratorState.Seed; DateTimeOffset start2 = HoursAfterMidnight(0); DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); Playout result2 = await builder.Build( result, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); primaryAnchor = result2.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); int secondSeedValue = primaryAnchor.EnumeratorState.Seed; firstSeedValue.Should().Be(secondSeedValue); primaryAnchor.EnumeratorState.Index.Should().Be(0); } [Test] public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays() { var mediaItems = new List(); for (var i = 1; i <= 100; i++) { mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); } (PlayoutBuilder builder, Playout playout) = TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle); DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); DateTimeOffset finish = start + TimeSpan.FromDays(2); Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish, _cancellationToken); result.Items.Count.Should().Be(53); result.ProgramScheduleAnchors.Count.Should().Be(4); result.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).Should().BeTrue(); PlayoutProgramScheduleAnchor lastCheckpoint = result.ProgramScheduleAnchors .Filter(psa => psa.SmartCollectionId == 1) .OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue) .First(); lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0); lastCheckpoint.EnumeratorState.Index.Should().Be(53); int firstSeedValue = lastCheckpoint.EnumeratorState.Seed; for (var i = 1; i < 20; i++) { DateTimeOffset start2 = start.AddHours(i); DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); Playout result2 = await builder.Build( result, PlayoutBuildMode.Continue, start2, finish2, _cancellationToken); PlayoutProgramScheduleAnchor continueAnchor = result2.ProgramScheduleAnchors .Filter(psa => psa.SmartCollectionId == 1) .First(x => x.AnchorDate is null); int secondSeedValue = continueAnchor.EnumeratorState.Seed; // the continue anchor should have the same seed as the most recent (last) checkpoint from the first run firstSeedValue.Should().Be(secondSeedValue); } } [Test] public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor() { var floodCollection = new Collection { Id = 1, Name = "Flood Items", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) } }; var fixedCollection = new Collection { Id = 2, Name = "Fixed Items", MediaItems = new List { TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (floodCollection.Id, floodCollection.MediaItems.ToList()), (fixedCollection.Id, fixedCollection.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemFlood { Id = 1, Index = 1, Collection = floodCollection, CollectionId = floodCollection.Id, StartTime = TimeSpan.FromHours(7), PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemOne { Id = 2, Index = 2, Collection = fixedCollection, CollectionId = fixedCollection.Id, StartTime = TimeSpan.FromHours(12), PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(9).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, InFlood = true }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(32); Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken); result.Items.Count.Should().Be(5); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(9)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(10)); result.Items[1].MediaItemId.Should().Be(2); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(11)); result.Items[2].MediaItemId.Should().Be(1); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(12)); result.Items[3].MediaItemId.Should().Be(3); result.Items[4].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(7)); result.Items[4].MediaItemId.Should().Be(2); result.Anchor.InFlood.Should().BeTrue(); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(32)); } [Test] public async Task Alternating_MultipleContent_Should_Maintain_Counts() { var collectionOne = new Collection { Id = 1, Name = "Multiple Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var collectionTwo = new Collection { Id = 2, Name = "Multiple Items 2", MediaItems = new List { TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemMultiple { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, Count = 3, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemMultiple { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, Count = 3, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(1).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, MultipleRemaining = 2 }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken); result.Items.Count.Should().Be(4); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[1].MediaItemId.Should().Be(1); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[2].MediaItemId.Should().Be(2); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[3].MediaItemId.Should().Be(2); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.MultipleRemaining.Should().Be(1); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } [Test] public async Task Alternating_Duration_Should_Maintain_Duration() { var collectionOne = new Collection { Id = 1, Name = "Duration Items 1", MediaItems = new List { TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var collectionTwo = new Collection { Id = 2, Name = "Duration Items 2", MediaItems = new List { TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)) } }; var fakeRepository = new FakeMediaCollectionRepository( Map( (collectionOne.Id, collectionOne.MediaItems.ToList()), (collectionTwo.Id, collectionTwo.MediaItems.ToList()))); var items = new List { new ProgramScheduleItemDuration { Id = 1, Index = 1, Collection = collectionOne, CollectionId = collectionOne.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), TailMode = TailMode.None, PlaybackOrder = PlaybackOrder.Chronological }, new ProgramScheduleItemDuration { Id = 2, Index = 2, Collection = collectionTwo, CollectionId = collectionTwo.Id, StartTime = null, PlayoutDuration = TimeSpan.FromHours(3), TailMode = TailMode.None, PlaybackOrder = PlaybackOrder.Chronological } }; var playout = new Playout { ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Anchor = new PlayoutAnchor { NextStart = HoursAfterMidnight(1).UtcDateTime, ScheduleItemsEnumeratorState = new CollectionEnumeratorState { Index = 0, Seed = 1 }, DurationFinish = HoursAfterMidnight(3).UtcDateTime }, ProgramScheduleAnchors = new List(), Items = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; IConfigElementRepository configRepo = Substitute.For(); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, fakeRepository, televisionRepo, artistRepo, factory, localFileSystem, _logger); DateTimeOffset start = HoursAfterMidnight(0); DateTimeOffset finish = start + TimeSpan.FromHours(5); Playout result = await builder.Build(playout, PlayoutBuildMode.Continue, start, finish, _cancellationToken); result.Items.Count.Should().Be(4); result.Items[0].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(1)); result.Items[0].MediaItemId.Should().Be(1); result.Items[1].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(2)); result.Items[1].MediaItemId.Should().Be(1); result.Items[2].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(3)); result.Items[2].MediaItemId.Should().Be(2); result.Items[3].StartOffset.TimeOfDay.Should().Be(TimeSpan.FromHours(4)); result.Items[3].MediaItemId.Should().Be(2); result.Anchor.ScheduleItemsEnumeratorState.Index.Should().Be(1); result.Anchor.DurationFinish.Should().Be(HoursAfterMidnight(6).UtcDateTime); result.Anchor.NextStartOffset.Should().Be(HoursAfterMidnight(5)); } } private static DateTimeOffset HoursAfterMidnight(int hours) { DateTimeOffset now = DateTimeOffset.Now; return now - now.TimeOfDay + TimeSpan.FromHours(hours); } private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) => new ProgramScheduleItemFlood { Id = 1, Index = 1, CollectionType = ProgramScheduleItemCollectionType.Collection, Collection = mediaCollection, CollectionId = mediaCollection.Id, StartTime = null, PlaybackOrder = playbackOrder }; private static ProgramScheduleItem Flood( SmartCollection smartCollection, SmartCollection fillerCollection, PlaybackOrder playbackOrder) => new ProgramScheduleItemFlood { Id = 1, Index = 1, CollectionType = ProgramScheduleItemCollectionType.SmartCollection, SmartCollection = smartCollection, SmartCollectionId = smartCollection.Id, StartTime = null, PlaybackOrder = playbackOrder, FallbackFiller = new FillerPreset { Id = 1, CollectionType = ProgramScheduleItemCollectionType.SmartCollection, SmartCollection = fillerCollection, SmartCollectionId = fillerCollection.Id, FillerKind = FillerKind.Fallback } }; private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => new() { Id = id, MovieMetadata = new List { new() { ReleaseDate = aired } }, MediaVersions = new List { new() { Duration = duration, MediaFiles = new List { new() { Path = $"/fake/path/{id}" } } } } }; private TestData TestDataFloodForItems( List mediaItems, PlaybackOrder playbackOrder, IConfigElementRepository configMock = null) { var mediaCollection = new Collection { Id = 1, MediaItems = mediaItems }; IConfigElementRepository configRepo = configMock ?? Substitute.For(); var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems))); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, collectionRepo, televisionRepo, artistRepo, factory, localFileSystem, _logger); var items = new List { Flood(mediaCollection, playbackOrder) }; var playout = new Playout { Id = 1, ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Items = new List(), ProgramScheduleAnchors = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; return new TestData(builder, playout); } private TestData TestDataFloodForSmartCollectionItems( List mediaItems, PlaybackOrder playbackOrder, IConfigElementRepository configMock = null) { var mediaCollection = new SmartCollection { Id = 1, Query = "asdf" }; var fillerCollection = new SmartCollection { Id = 2, Query = "ghjk" }; IConfigElementRepository configRepo = configMock ?? Substitute.For(); var collectionRepo = new FakeMediaCollectionRepository( Map( (mediaCollection.Id, mediaItems), (fillerCollection.Id, mediaItems.Take(1).ToList()) ) ); var televisionRepo = new FakeTelevisionRepository(); IArtistRepository artistRepo = Substitute.For(); IMultiEpisodeShuffleCollectionEnumeratorFactory factory = Substitute.For(); ILocalFileSystem localFileSystem = Substitute.For(); var builder = new PlayoutBuilder( configRepo, collectionRepo, televisionRepo, artistRepo, factory, localFileSystem, _logger); var items = new List { Flood(mediaCollection, fillerCollection, playbackOrder) }; var playout = new Playout { Id = 1, ProgramSchedule = new ProgramSchedule { Items = items }, Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, Items = new List(), ProgramScheduleAnchors = new List(), ProgramScheduleAlternates = new List(), FillGroupIndices = [] }; return new TestData(builder, playout); } private record TestData(PlayoutBuilder Builder, Playout Playout); }