From f40eaef898d35ef2ac5adbcc0ad4c94ceab72ea5 Mon Sep 17 00:00:00 2001 From: embolon Date: Tue, 10 Sep 2024 11:36:20 -0700 Subject: [PATCH] [scheduling] Add a new mode RandomRotation that randomly picks an item from a randomly choosen group (show/artist) for block schedule (#1885) * init * minor naming change * address to comments round 1 * update dependencies * formatting * make sure it rotates * update changelog --------- Co-authored-by: Jason Dove <1695733+jasongdove@users.noreply.github.com> --- CHANGELOG.md | 3 + .../ErsatzTV.Application.csproj | 2 +- .../ProgramScheduleItemCommandBase.cs | 1 + .../ErsatzTV.Core.Tests.csproj | 4 +- ErsatzTV.Core/Domain/PlaybackOrder.cs | 3 +- ErsatzTV.Core/ErsatzTV.Core.csproj | 2 +- .../BlockScheduling/BlockPlayoutBuilder.cs | 9 +- .../BlockScheduling/BlockPlayoutEnumerator.cs | 27 ++++++ ...omizedRotatingMediaCollectionEnumerator.cs | 96 +++++++++++++++++++ .../ErsatzTV.FFmpeg.Tests.csproj | 4 +- .../ErsatzTV.Infrastructure.Tests.csproj | 4 +- .../ErsatzTV.Infrastructure.csproj | 2 +- .../ErsatzTV.Scanner.Tests.csproj | 4 +- ErsatzTV.Scanner/ErsatzTV.Scanner.csproj | 2 +- ErsatzTV/ErsatzTV.csproj | 4 +- ErsatzTV/Pages/BlockEditor.razor | 1 + 16 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index b8f0670ca..8ace5fdd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Add `Random Count` filler mode by @embolon - This mode will randomly schedule between zero and the provided count number of items - e.g. random count 3 will schedule between 0 and 3 filler items +- Add `Random Rotation` playback order for block scheduling by @embolon + - This playback order will pick a random item from a randomly selected group (show or artist) + - It is somewhat similar to the `Fill With Group` mode used in flood scheduling ### Fixed - Add basic cache busting to XMLTV image URLs diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj b/ErsatzTV.Application/ErsatzTV.Application.csproj index acaacdba4..11e5424d2 100644 --- a/ErsatzTV.Application/ErsatzTV.Application.csproj +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj @@ -12,7 +12,7 @@ - + diff --git a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs index a84ce8389..f69639064 100644 --- a/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs +++ b/ErsatzTV.Application/ProgramSchedules/Commands/ProgramScheduleItemCommandBase.cs @@ -57,6 +57,7 @@ public abstract class ProgramScheduleItemCommandBase case PlaybackOrder.Random: case PlaybackOrder.MultiEpisodeShuffle: case PlaybackOrder.SeasonEpisode: + case PlaybackOrder.RandomRotation: return BaseError.New($"Invalid playback order for multi collection: '{item.PlaybackOrder}'"); case PlaybackOrder.Shuffle: case PlaybackOrder.ShuffleInOrder: diff --git a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj index a22a243ee..a237a8913 100644 --- a/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj +++ b/ErsatzTV.Core.Tests/ErsatzTV.Core.Tests.csproj @@ -9,14 +9,14 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ErsatzTV.Core/Domain/PlaybackOrder.cs b/ErsatzTV.Core/Domain/PlaybackOrder.cs index 7568e592d..aea085e15 100644 --- a/ErsatzTV.Core/Domain/PlaybackOrder.cs +++ b/ErsatzTV.Core/Domain/PlaybackOrder.cs @@ -7,5 +7,6 @@ public enum PlaybackOrder Shuffle = 3, ShuffleInOrder = 4, MultiEpisodeShuffle = 5, - SeasonEpisode = 6 + SeasonEpisode = 6, + RandomRotation = 7 } diff --git a/ErsatzTV.Core/ErsatzTV.Core.csproj b/ErsatzTV.Core/ErsatzTV.Core.csproj index acc6ca9d0..e4636eb4b 100644 --- a/ErsatzTV.Core/ErsatzTV.Core.csproj +++ b/ErsatzTV.Core/ErsatzTV.Core.csproj @@ -14,7 +14,7 @@ - + diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index 009474240..32b116b3b 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -43,7 +43,8 @@ public class BlockPlayoutBuilder( PlaybackOrder.Chronological, PlaybackOrder.SeasonEpisode, PlaybackOrder.Shuffle, - PlaybackOrder.Random + PlaybackOrder.Random, + PlaybackOrder.RandomRotation ]; DateTimeOffset start = DateTimeOffset.Now; @@ -264,6 +265,12 @@ public class BlockPlayoutBuilder( playout, blockItem, historyKey), + PlaybackOrder.RandomRotation => BlockPlayoutEnumerator.RandomRotation( + collectionItems, + currentTime, + playout, + blockItem, + historyKey), _ => new RandomizedMediaCollectionEnumerator( collectionItems, new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 }) diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs index 676944f40..b91896c7b 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutEnumerator.cs @@ -137,4 +137,31 @@ public static class BlockPlayoutEnumerator // as long as already-played items are not included return new BlockPlayoutShuffledMediaCollectionEnumerator(mediaItems, state); } + + public static IMediaCollectionEnumerator RandomRotation( + List collectionItems, + DateTimeOffset currentTime, + Playout playout, + BlockItem blockItem, + string historyKey) + { + DateTime historyTime = currentTime.UtcDateTime; + Option maybeHistory = playout.PlayoutHistory + .Filter(h => h.BlockId == blockItem.BlockId) + .Filter(h => h.Key == historyKey) + .Filter(h => h.When < historyTime) + .OrderByDescending(h => h.When) + .HeadOrNone(); + + var state = new CollectionEnumeratorState { Seed = playout.Seed + blockItem.BlockId, Index = 0 }; + foreach (PlayoutHistory h in maybeHistory) + { + // Make sure to only increase the index by 1 since we can only + // guarantee the next one is a different show. h.Index comes from + // the previous play item which already increased by 1. + state.Index = h.Index; + } + + return new RandomizedRotatingMediaCollectionEnumerator(collectionItems, state); + } } diff --git a/ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs new file mode 100644 index 000000000..efe29917f --- /dev/null +++ b/ErsatzTV.Core/Scheduling/RandomizedRotatingMediaCollectionEnumerator.cs @@ -0,0 +1,96 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Extensions; +using ErsatzTV.Core.Interfaces.Scheduling; + +namespace ErsatzTV.Core.Scheduling; + +public class RandomizedRotatingMediaCollectionEnumerator : IMediaCollectionEnumerator +{ + private readonly Lazy> _lazyMinimumDuration; + private readonly IList _mediaItems; + private readonly Random _random; + private readonly Dictionary> _groupMedia; + private int _index; + private int _groupNumber; + + public RandomizedRotatingMediaCollectionEnumerator(IList mediaItems, CollectionEnumeratorState state) + { + CurrentIncludeInProgramGuide = Option.None; + + _mediaItems = mediaItems; + _lazyMinimumDuration = + new Lazy>( + () => _mediaItems.Bind(i => i.GetNonZeroDuration()).OrderBy(identity).HeadOrNone()); + _random = new Random(state.Seed); + + _groupMedia = new Dictionary>(); + for (int i = 0; i < mediaItems.Count; i++) + { + int id = mediaItems[i] switch + { + Episode e => e.Season.ShowId, + MusicVideo mv => mv.ArtistId, + _ => mediaItems[i].Id + }; + + if (_groupMedia.TryGetValue(id, out IList newList)) + { + newList.Add(i); + } + else + { + _groupMedia.Add(id, new List { i }); + } + } + + _groupNumber = 0; + + State = new CollectionEnumeratorState { Seed = state.Seed }; + // we want to move at least once so we start with a random item and not the first + // because _index defaults to 0 + while (State.Index <= state.Index) + { + MoveNext(); + } + } + + public void ResetState(CollectionEnumeratorState state) => + // seed never changes here, no need to reset + State.Index = state.Index; + + public CollectionEnumeratorState State { get; } + + public Option Current => _mediaItems.Any() ? _mediaItems[_index] : None; + public Option CurrentIncludeInProgramGuide { get; } + + public void MoveNext() + { + IList groups = _groupMedia.Keys.ToList(); + int nextRandom = _random.Next(); + + int groupNumber = nextRandom % groups.Count; + if (_groupNumber == groupNumber) + { + if (groupNumber == groups.Count - 1) + { + _groupNumber = 0; + } + else + { + _groupNumber = groupNumber + 1; + } + } + else + { + _groupNumber = groupNumber; + } + + int itemNumber = nextRandom % _groupMedia[groups[_groupNumber]].Count; + _index = _groupMedia[groups[_groupNumber]][itemNumber]; + State.Index++; + } + + public Option MinimumDuration => _lazyMinimumDuration.Value; + + public int Count => _mediaItems.Count; +} diff --git a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj index 915fa39c9..336edf49a 100644 --- a/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj +++ b/ErsatzTV.FFmpeg.Tests/ErsatzTV.FFmpeg.Tests.csproj @@ -8,9 +8,9 @@ - + - + diff --git a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj index b164b49b0..ec5e0f673 100644 --- a/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj +++ b/ErsatzTV.Infrastructure.Tests/ErsatzTV.Infrastructure.Tests.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index 7b9c46a9f..c74becd8a 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -13,7 +13,7 @@ - + diff --git a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj index f02254c3d..c9435ad9e 100644 --- a/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj +++ b/ErsatzTV.Scanner.Tests/ErsatzTV.Scanner.Tests.csproj @@ -9,9 +9,9 @@ - + - + diff --git a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj index c382fe0ca..b1b61da18 100644 --- a/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj +++ b/ErsatzTV.Scanner/ErsatzTV.Scanner.csproj @@ -23,7 +23,7 @@ - + diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 256ef0e31..f8d42f7cf 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -20,7 +20,7 @@ - + @@ -37,7 +37,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/ErsatzTV/Pages/BlockEditor.razor b/ErsatzTV/Pages/BlockEditor.razor index d1622ac2c..88f918b25 100644 --- a/ErsatzTV/Pages/BlockEditor.razor +++ b/ErsatzTV/Pages/BlockEditor.razor @@ -230,6 +230,7 @@ Chronological Shuffle Random + Random Rotation @* Shuffle In Order *@ break; case ProgramScheduleItemCollectionType.TelevisionShow: