diff --git a/CHANGELOG.md b/CHANGELOG.md index 8deb5cd8f..931d23b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Respect `z_index` (draw order) on all graphics element types - Fix bug with `z_index` sorting - Restore default UI font that was erroneously removed in v26.1.1 +- Classic schedules: fix building playouts when `Fill With Group Mode` schedule items also have graphics elements - Use configured searching log level on startup, instead of the default log level of `Information` - MySql: fix searching for shows and seasons in schedule items editor - Fix 500 errors when serving XMLTV due to concurrent file reads and writes diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs index 509937dfe..db87f508c 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs @@ -76,6 +76,8 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository public Task IsCustomPlaybackOrder(int collectionId) => false.AsTask(); public Task> GetNameFromKey(CollectionKey emptyCollection, CancellationToken cancellationToken) => Option.None.AsTask(); - public List GroupIntoFakeCollections(List items, string fakeKey = null) => - throw new NotSupportedException(); + public List GroupIntoFakeCollections(List items, string fakeKey = null) + { + return [new CollectionWithItems(1, 0, fakeKey, items, true, PlaybackOrder.Shuffle, false)]; + } } diff --git a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs index e1da25e24..bf3a4d225 100644 --- a/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/ClassicScheduling/NewPlayoutTests.cs @@ -15,6 +15,103 @@ namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; [TestFixture] public class NewPlayoutTests : PlayoutBuilderTestBase { + [Test] + public async Task FillWithGroupMode_Should_Not_Fail() + { + var collection = new Collection + { + Id = 1, + Name = "Multiple Items", + MediaItems = + [ + TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), + TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) + ] + }; + + var fakeRepository = new FakeMediaCollectionRepository( + Map((collection.Id, collection.MediaItems.ToList()))); + + var items = new List + { + new ProgramScheduleItemMultiple + { + Id = 1, + Index = 1, + //Collection = collection, + CollectionId = collection.Id, + StartTime = null, + PlaybackOrder = PlaybackOrder.Chronological, + MultipleMode = MultipleMode.Count, + Count = "2", + FillWithGroupMode = FillWithGroupMode.FillWithShuffledGroups, + ProgramScheduleItemGraphicsElements = [] + } + }; + + // having a graphics element reference the schedule item triggers the bug + items[0].ProgramScheduleItemGraphicsElements.Add(new ProgramScheduleItemGraphicsElement + { + GraphicsElementId = 1, + ProgramScheduleItem = items[0], + ProgramScheduleItemId = items[0].Id + }); + + var playout = new Playout + { + ProgramSchedule = new ProgramSchedule + { + Items = items + }, + Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, + ProgramScheduleAnchors = [], + Items = [], + ProgramScheduleAlternates = [], + FillGroupIndices = [] + }; + + var referenceData = + new PlayoutReferenceData( + playout.Channel, + Option.None, + [], + [], + playout.ProgramSchedule, + [], + [], + TimeSpan.Zero); + + IConfigElementRepository configRepo = Substitute.For(); + var televisionRepo = new FakeTelevisionRepository(); + IArtistRepository artistRepo = Substitute.For(); + IMultiEpisodeShuffleCollectionEnumeratorFactory factory = + Substitute.For(); + IRerunHelper rerunHelper = Substitute.For(); + var builder = new PlayoutBuilder( + configRepo, + fakeRepository, + televisionRepo, + artistRepo, + factory, + new MockFileSystem(), + rerunHelper, + Logger); + + DateTimeOffset start = HoursAfterMidnight(0); + DateTimeOffset finish = start + TimeSpan.FromHours(6); + + Either buildResult = await builder.Build( + playout, + referenceData, + PlayoutBuildResult.Empty, + PlayoutBuildMode.Reset, + start, + finish, + CancellationToken); + + buildResult.IsRight.ShouldBeTrue(); + } + [Test] public async Task OnlyZeroDurationItem_Should_Abort() { diff --git a/ErsatzTV.Core/Extensions/ProgramScheduleItemExtensions.cs b/ErsatzTV.Core/Extensions/ProgramScheduleItemExtensions.cs new file mode 100644 index 000000000..53f2ffc06 --- /dev/null +++ b/ErsatzTV.Core/Extensions/ProgramScheduleItemExtensions.cs @@ -0,0 +1,25 @@ +using ErsatzTV.Core.Domain; +using Newtonsoft.Json; + +namespace ErsatzTV.Core.Extensions; + +public static class ProgramScheduleItemExtensions +{ + public static ProgramScheduleItem DeepCopy(this ProgramScheduleItem item) + { + if (item == null) + { + return null; + } + + var settings = new JsonSerializerSettings + { + // program schedule item => graphics element => (same) program schedule item should be ignored + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore + }; + + string json = JsonConvert.SerializeObject(item, settings); + return (ProgramScheduleItem)JsonConvert.DeserializeObject(json, item.GetType(), settings); + } +} diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 16add392d..1d467f567 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1,5 +1,4 @@ using System.IO.Abstractions; -using System.Reflection; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Errors; @@ -673,13 +672,6 @@ public class PlayoutBuilder : IPlayoutBuilder .Filter(c => c.ShowId > 0 || c.ArtistId > 0 || !string.IsNullOrWhiteSpace(c.Key)) .ToList(); List fakeScheduleItems = []; - - // this will be used to clone a schedule item - MethodInfo generic = typeof(JsonConvert).GetMethods() - .FirstOrDefault(x => x.Name.Equals("DeserializeObject", StringComparison.OrdinalIgnoreCase) && - x.IsGenericMethod && - x.GetParameters().Length == 1)?.MakeGenericMethod(scheduleItem.GetType()); - foreach (CollectionWithItems fakeCollection in fakeCollections) { CollectionKey key = (fakeCollection.ShowId, fakeCollection.ArtistId, fakeCollection.Key) switch @@ -709,8 +701,7 @@ public class PlayoutBuilder : IPlayoutBuilder continue; } - string serialized = JsonConvert.SerializeObject(scheduleItem); - var copyScheduleItem = generic.Invoke(this, [serialized]) as ProgramScheduleItem; + var copyScheduleItem = scheduleItem.DeepCopy(); copyScheduleItem.CollectionType = key.CollectionType; copyScheduleItem.MediaItemId = key.MediaItemId; copyScheduleItem.FakeCollectionKey = key.FakeCollectionKey;