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 @@
-
+
-
+ allruntime; 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
-
+
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 @@
ChronologicalShuffleRandom
+ Random Rotation
@* Shuffle In Order *@
break;
case ProgramScheduleItemCollectionType.TelevisionShow: