diff --git a/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs index 90f3e3a20..c12593b1b 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs @@ -362,6 +362,101 @@ public class PlaylistEnumeratorTests enumerator.CountForFiller.ShouldBe(7); } + [Test] + public async Task RandomStartPoint_Should_Continue_After_Rebuild() + { + IMediaCollectionRepository repo = Substitute.For(); + + Dictionary> BuildMap() => new() + { + { + new PlaylistItem + { + Id = 1, + Index = 0, + PlaybackOrder = PlaybackOrder.Chronological, + PlayAll = false, + CollectionType = CollectionType.Collection, + CollectionId = 1 + }, + [FakeMovie(10), FakeMovie(11), FakeMovie(12), FakeMovie(13), FakeMovie(14)] + }, + { + new PlaylistItem + { + Id = 2, + Index = 1, + PlaybackOrder = PlaybackOrder.Chronological, + PlayAll = false, + CollectionType = CollectionType.Collection, + CollectionId = 2 + }, + [FakeMovie(15), FakeMovie(16), FakeMovie(17), FakeMovie(18), FakeMovie(19)] + }, + { + new PlaylistItem + { + Id = 3, + Index = 2, + PlaybackOrder = PlaybackOrder.Chronological, + PlayAll = false, + CollectionType = CollectionType.Collection, + CollectionId = 3 + }, + [FakeMovie(20), FakeMovie(21), FakeMovie(22), FakeMovie(23), FakeMovie(24)] + } + }; + + const int SEED = 987654321; + + // day 1: fresh build with a random start point + PlaylistEnumerator day1 = await PlaylistEnumerator.Create( + repo, + BuildMap(), + new CollectionEnumeratorState { Seed = SEED, Index = 0 }, + shufflePlaylistItems: false, + batchSize: Option.None, + randomStartPoint: true, + CancellationToken.None); + + // capture a reference sequence and the state we would persist halfway through + var reference = new List(); + CollectionEnumeratorState persistedState = null; + for (var i = 0; i < 12; i++) + { + reference.AddRange(day1.Current.Map(mi => mi.Id)); + day1.MoveNext(Option.None); + if (i == 5) + { + persistedState = day1.State.Clone(); + } + } + + // sanity: the random start point actually moved us off the natural start + reference.First().ShouldNotBe(10); + persistedState.Started.ShouldBeTrue(); + + // day 2: rebuild from the persisted state; the offsets must be reproduced so we continue + PlaylistEnumerator day2 = await PlaylistEnumerator.Create( + repo, + BuildMap(), + persistedState, + shufflePlaylistItems: false, + batchSize: Option.None, + randomStartPoint: true, + CancellationToken.None); + + var continued = new List(); + for (var i = 0; i < 6; i++) + { + continued.AddRange(day2.Current.Map(mi => mi.Id)); + day2.MoveNext(Option.None); + } + + // day 2 must pick up exactly where day 1 left off, not reset toward the start + continued.ShouldBe(reference.Skip(6).Take(6)); + } + private static Movie FakeMovie(int id) => new() { Id = id, diff --git a/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs b/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs index 62cb8f682..21212cbb0 100644 --- a/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs @@ -137,9 +137,8 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator _batchSize = batchSize }; - // random start points only apply to a fresh build with no saved state + // random start points must be applied on every build, not just the first var random = new Random(state.Seed); - randomStartPoint = randomStartPoint && !state.Started; // collections should share enumerators var enumeratorMap = new Dictionary(); diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index d1c46d428..b60b3ac83 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1369,7 +1369,7 @@ public class PlayoutBuilder : IPlayoutBuilder collectionKey, cancellationToken), state, - randomStartPoint, + activeSchedule.RandomStartPoint, cancellationToken); case PlaybackOrder.MultiEpisodeShuffle when collectionKey.CollectionType == CollectionType.TelevisionShow &&