diff --git a/CHANGELOG.md b/CHANGELOG.md index b27c598f5..46e9383f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Update Plex movie and other video titles when changed from Plex - The first library scan after updating will act like a deep scan due to adding title to the Plex etag calculation - Future periodic scans will update titles in ETV automatically (deep scans will not be required to update titles) +- Randomize start points on playlist items in classic schedules when setting is enabled + - Playlists ignored this setting in earlier builds ## [26.6.0] - 2026-07-09 ### Added diff --git a/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs b/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs index 1d174815b..90f3e3a20 100644 --- a/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs +++ b/ErsatzTV.Core.Tests/Scheduling/PlaylistEnumeratorTests.cs @@ -62,6 +62,7 @@ public class PlaylistEnumeratorTests new CollectionEnumeratorState(), shufflePlaylistItems: false, batchSize: Option.None, + randomStartPoint: false, CancellationToken.None); var items = new List(); @@ -130,6 +131,7 @@ public class PlaylistEnumeratorTests new CollectionEnumeratorState(), shufflePlaylistItems: false, batchSize: Option.None, + randomStartPoint: false, CancellationToken.None); var items = new List(); @@ -200,6 +202,7 @@ public class PlaylistEnumeratorTests state, shufflePlaylistItems: true, batchSize: Option.None, + randomStartPoint: false, CancellationToken.None); var items = new List(); @@ -270,6 +273,7 @@ public class PlaylistEnumeratorTests state, shufflePlaylistItems: true, batchSize: Option.None, + randomStartPoint: false, CancellationToken.None); var items = new List(); @@ -352,6 +356,7 @@ public class PlaylistEnumeratorTests state, shufflePlaylistItems: true, batchSize: Option.None, + randomStartPoint: false, CancellationToken.None); enumerator.CountForFiller.ShouldBe(7); diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs index 2cb096bad..4c6586f42 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs @@ -168,6 +168,7 @@ public static class BlockPlayoutEnumerator state, shufflePlaylistItems: false, batchSize: Option.None, + randomStartPoint: false, cancellationToken); DateTime historyTime = currentTime.UtcDateTime; diff --git a/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs b/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs index e7975327c..f67c5b04b 100644 --- a/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs +++ b/ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs @@ -66,6 +66,7 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository state, marathonShuffleGroups, marathonBatchSize, + randomStartPoint: false, cancellationToken); } @@ -136,6 +137,7 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository state, shuffleGroups, batchSize: Option.None, + randomStartPoint: false, cancellationToken); return new PlaylistContentResult( diff --git a/ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs b/ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs index cb1bfa06a..d7ff4363d 100644 --- a/ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs +++ b/ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs @@ -58,6 +58,7 @@ public class PlaylistHelper(IMediaCollectionRepository mediaCollectionRepository state, shufflePlaylistItems: false, batchSize: Option.None, + randomStartPoint: false, cancellationToken); return new PlaylistContentResult( diff --git a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs index 53837822e..2139f65c9 100644 --- a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs +++ b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs @@ -257,6 +257,7 @@ public class SchedulingEngine( state, shufflePlaylistItems: false, batchSize: Option.None, + randomStartPoint: false, CancellationToken.None); string historyKey = HistoryDetails.KeyForSchedulingContent(key, PlaybackOrder.None); diff --git a/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs b/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs index 44706de67..194a99eb3 100644 --- a/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/PlaylistEnumerator.cs @@ -121,6 +121,7 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator CollectionEnumeratorState state, bool shufflePlaylistItems, Option batchSize, + bool randomStartPoint, CancellationToken cancellationToken) { var result = new PlaylistEnumerator @@ -131,6 +132,10 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator _batchSize = batchSize }; + // random start points only apply to a fresh build with no saved state + var random = new Random(state.Seed); + randomStartPoint = randomStartPoint && state.Index == 0; + // collections should share enumerators var enumeratorMap = new Dictionary(); result._allMediaItemIds = []; @@ -166,6 +171,11 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator switch (playlistItem.PlaybackOrder) { case PlaybackOrder.Chronological: + if (randomStartPoint) + { + initState.Index = random.Next(0, items.Count - 1); + } + enumerator = new ChronologicalMediaCollectionEnumerator(items, initState); break; // TODO: fix multi episode shuffle? @@ -187,18 +197,22 @@ public class PlaylistEnumerator : IMediaCollectionEnumerator CollectionKey.ForPlaylistItem(playlistItem), cancellationToken), initState, - // TODO: fix this - false, + randomStartPoint, cancellationToken); break; case PlaybackOrder.SeasonEpisode: - // TODO: check random start point? + if (randomStartPoint) + { + initState.Index = random.Next(0, items.Count - 1); + } + enumerator = new SeasonEpisodeMediaCollectionEnumerator(items, initState); // season, episode will filter out season 0, so we may get an empty enumerator back if (enumerator.Count == 0) { enumerator = null; } + break; case PlaybackOrder.Random: enumerator = new RandomizedMediaCollectionEnumerator(items, initState); diff --git a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs index 582eb65d1..bfdb642dc 100644 --- a/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/PlayoutBuilder.cs @@ -1325,6 +1325,7 @@ public class PlayoutBuilder : IPlayoutBuilder state, marathonShuffleGroups, batchSize: Option.None, + randomStartPoint, cancellationToken); } } diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs index 9c394253c..af3d9c09e 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs @@ -175,6 +175,7 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor state, shufflePlaylistItems: false, batchSize: Option.None, + randomStartPoint: false, cancellationToken); }