add option to shuffle schedule items (#652)

* add schedule setting

* it works

* fix tests

* update readme

* rebuild all playouts
This commit is contained in:
Jason Dove
2022-02-22 21:20:40 -06:00
committed by GitHub
parent f6c42f3ff5
commit 78fdc9c57a
39 changed files with 12572 additions and 375 deletions
@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling;
public class OrderedScheduleItemsEnumerator : IScheduleItemsEnumerator
{
private readonly IList<ProgramScheduleItem> _sortedScheduleItems;
public OrderedScheduleItemsEnumerator(
IEnumerable<ProgramScheduleItem> scheduleItems,
CollectionEnumeratorState state)
{
_sortedScheduleItems = scheduleItems.OrderBy(i => i.Index).ToList();
State = new CollectionEnumeratorState { Seed = state.Seed };
if (state.Index >= _sortedScheduleItems.Count)
{
state.Index = 0;
state.Seed = 0;
}
while (State.Index < state.Index)
{
MoveNext();
}
}
public CollectionEnumeratorState State { get; }
public ProgramScheduleItem Current => _sortedScheduleItems[State.Index];
public void MoveNext() => State.Index = (State.Index + 1) % _sortedScheduleItems.Count;
public ProgramScheduleItem Peek(int offset) =>
_sortedScheduleItems[(State.Index + offset) % _sortedScheduleItems.Count];
}
+17 -18
View File
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt;
@@ -105,6 +104,12 @@ namespace ErsatzTV.Core.Scheduling
}
var sortedScheduleItems = playout.ProgramSchedule.Items.OrderBy(i => i.Index).ToList();
CollectionEnumeratorState scheduleItemsEnumeratorState =
playout.Anchor?.ScheduleItemsEnumeratorState ?? new CollectionEnumeratorState
{ Seed = Random.Next(), Index = 0 };
IScheduleItemsEnumerator scheduleItemsEnumerator = playout.ProgramSchedule.ShuffleScheduleItems
? new ShuffledScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState)
: new OrderedScheduleItemsEnumerator(playout.ProgramSchedule.Items, scheduleItemsEnumeratorState);
var collectionEnumerators = new Dictionary<CollectionKey, IMediaCollectionEnumerator>();
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in collectionMediaItems)
{
@@ -119,7 +124,7 @@ namespace ErsatzTV.Core.Scheduling
}
// find start anchor
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, sortedScheduleItems);
PlayoutAnchor startAnchor = FindStartAnchor(playout, playoutStart, scheduleItemsEnumerator);
// start at the previously-decided time
DateTimeOffset currentTime = startAnchor.NextStartOffset.ToLocalTime();
@@ -142,7 +147,7 @@ namespace ErsatzTV.Core.Scheduling
// start with the previously-decided schedule item
// start with the previous multiple/duration states
var playoutBuilderState = new PlayoutBuilderState(
sortedScheduleItems.IndexOf(startAnchor.NextScheduleItem),
scheduleItemsEnumerator,
Optional(startAnchor.MultipleRemaining),
startAnchor.DurationFinishOffset,
startAnchor.InFlood,
@@ -153,17 +158,15 @@ namespace ErsatzTV.Core.Scheduling
var schedulerOne = new PlayoutModeSchedulerOne(_logger);
var schedulerMultiple = new PlayoutModeSchedulerMultiple(collectionMediaItems, _logger);
var schedulerDuration = new PlayoutModeSchedulerDuration(_logger);
var schedulerFlood = new PlayoutModeSchedulerFlood(sortedScheduleItems, _logger);
var schedulerFlood = new PlayoutModeSchedulerFlood(_logger);
// loop until we're done filling the desired amount of time
while (playoutBuilderState.CurrentTime < playoutFinish)
{
// get the schedule item out of the sorted list
ProgramScheduleItem scheduleItem =
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
ProgramScheduleItem scheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
ProgramScheduleItem nextScheduleItem =
sortedScheduleItems[(playoutBuilderState.ScheduleItemIndex + 1) % sortedScheduleItems.Count];
ProgramScheduleItem nextScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Peek(1);
Tuple<PlayoutBuilderState, List<PlayoutItem>> result = scheduleItem switch
{
@@ -205,8 +208,7 @@ namespace ErsatzTV.Core.Scheduling
}
// once more to get playout anchor
ProgramScheduleItem anchorScheduleItem =
sortedScheduleItems[playoutBuilderState.ScheduleItemIndex % sortedScheduleItems.Count];
ProgramScheduleItem anchorScheduleItem = playoutBuilderState.ScheduleItemsEnumerator.Current;
// build program schedule anchors
playout.ProgramScheduleAnchors = BuildProgramScheduleAnchors(playout, collectionEnumerators);
@@ -226,8 +228,7 @@ namespace ErsatzTV.Core.Scheduling
playout.Anchor = new PlayoutAnchor
{
NextScheduleItem = anchorScheduleItem,
NextScheduleItemId = anchorScheduleItem.Id,
ScheduleItemsEnumeratorState = playoutBuilderState.ScheduleItemsEnumerator.State,
NextStart = PlayoutModeSchedulerBase<ProgramScheduleItem>.GetStartTimeAfter(playoutBuilderState, anchorScheduleItem)
.UtcDateTime,
MultipleRemaining = playoutBuilderState.MultipleRemaining.IsSome
@@ -320,18 +321,17 @@ namespace ErsatzTV.Core.Scheduling
private static PlayoutAnchor FindStartAnchor(
Playout playout,
DateTimeOffset start,
IReadOnlyCollection<ProgramScheduleItem> sortedScheduleItems) =>
IScheduleItemsEnumerator enumerator) =>
Optional(playout.Anchor).IfNone(
() =>
{
ProgramScheduleItem schedule = sortedScheduleItems.Head();
ProgramScheduleItem schedule = enumerator.Current;
switch (schedule.StartType)
{
case StartType.Fixed:
return new PlayoutAnchor
{
NextScheduleItem = schedule,
NextScheduleItemId = schedule.Id,
ScheduleItemsEnumeratorState = enumerator.State,
NextStart = (start - start.TimeOfDay).UtcDateTime +
schedule.StartTime.GetValueOrDefault()
};
@@ -339,8 +339,7 @@ namespace ErsatzTV.Core.Scheduling
default:
return new PlayoutAnchor
{
NextScheduleItem = schedule,
NextScheduleItemId = schedule.Id,
ScheduleItemsEnumeratorState = enumerator.State,
NextStart = (start - start.TimeOfDay).UtcDateTime
};
}
@@ -1,10 +1,11 @@
using System;
using ErsatzTV.Core.Interfaces.Scheduling;
using LanguageExt;
namespace ErsatzTV.Core.Scheduling
{
public record PlayoutBuilderState(
int ScheduleItemIndex,
IScheduleItemsEnumerator ScheduleItemsEnumerator,
Option<int> MultipleRemaining,
Option<DateTimeOffset> DurationFinish,
bool InFlood,
@@ -122,9 +122,10 @@ namespace ErsatzTV.Core.Scheduling
nextState = nextState with
{
DurationFinish = None,
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
DurationFinish = None
};
nextState.ScheduleItemsEnumerator.MoveNext();
}
}
@@ -133,9 +134,10 @@ namespace ErsatzTV.Core.Scheduling
{
nextState = nextState with
{
DurationFinish = None,
ScheduleItemIndex = nextState.ScheduleItemIndex + 1
DurationFinish = None
};
nextState.ScheduleItemsEnumerator.MoveNext();
}
if (playoutItems.Select(pi => pi.GuideGroup).Distinct().Count() != 1)
@@ -12,12 +12,9 @@ namespace ErsatzTV.Core.Scheduling
{
public class PlayoutModeSchedulerFlood : PlayoutModeSchedulerBase<ProgramScheduleItemFlood>
{
private readonly List<ProgramScheduleItem> _sortedScheduleItems;
public PlayoutModeSchedulerFlood(List<ProgramScheduleItem> sortedScheduleItems, ILogger logger)
public PlayoutModeSchedulerFlood(ILogger logger)
: base(logger)
{
_sortedScheduleItems = sortedScheduleItems;
}
public override Tuple<PlayoutBuilderState, List<PlayoutItem>> Schedule(
@@ -35,8 +32,7 @@ namespace ErsatzTV.Core.Scheduling
IMediaCollectionEnumerator contentEnumerator =
collectionEnumerators[CollectionKey.ForScheduleItem(scheduleItem)];
ProgramScheduleItem peekScheduleItem =
_sortedScheduleItems[(nextState.ScheduleItemIndex + 1) % _sortedScheduleItems.Count];
ProgramScheduleItem peekScheduleItem = nextScheduleItem;
while (contentEnumerator.Current.IsSome && nextState.CurrentTime < hardStop && willFinishInTime)
{
@@ -81,7 +77,7 @@ namespace ErsatzTV.Core.Scheduling
{
playoutItems.AddRange(
AddFiller(nextState, collectionEnumerators, scheduleItem, playoutItem, itemChapters));
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
DateTimeOffset actualEndTime = playoutItems.Max(p => p.FinishOffset);
if (Math.Abs((itemEndTimeWithFiller - actualEndTime).TotalSeconds) > 1)
@@ -105,19 +101,19 @@ namespace ErsatzTV.Core.Scheduling
}
}
// _logger.LogDebug(
// "Advancing to next schedule item after playout mode {PlayoutMode}",
// "Flood");
_logger.LogDebug(
"Advancing to next schedule item after playout mode {PlayoutMode}",
"Flood");
nextState = nextState with
{
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
InFlood = nextState.CurrentTime >= hardStop,
NextGuideGroup = nextState.DecrementGuideGroup
};
ProgramScheduleItem peekItem =
_sortedScheduleItems[nextState.ScheduleItemIndex % _sortedScheduleItems.Count];
nextState.ScheduleItemsEnumerator.MoveNext();
ProgramScheduleItem peekItem = nextScheduleItem;
DateTimeOffset peekItemStart = GetStartTimeAfter(nextState, peekItem);
if (scheduleItem.TailFiller != null)
@@ -97,10 +97,11 @@ namespace ErsatzTV.Core.Scheduling
nextState = nextState with
{
ScheduleItemIndex = nextState.ScheduleItemIndex + 1,
MultipleRemaining = None,
NextGuideGroup = nextState.DecrementGuideGroup
};
nextState.ScheduleItemsEnumerator.MoveNext();
}
DateTimeOffset nextItemStart = GetStartTimeAfter(nextState, nextScheduleItem);
@@ -67,10 +67,10 @@ namespace ErsatzTV.Core.Scheduling
PlayoutBuilderState nextState = playoutBuilderState with
{
CurrentTime = itemEndTimeWithFiller,
ScheduleItemIndex = playoutBuilderState.ScheduleItemIndex + 1
CurrentTime = itemEndTimeWithFiller
};
nextState.ScheduleItemsEnumerator.MoveNext();
contentEnumerator.MoveNext();
// LogScheduledItem(scheduleItem, mediaItem, itemStartTime);
@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling;
public class ShuffledScheduleItemsEnumerator : IScheduleItemsEnumerator
{
private readonly int _scheduleItemsCount;
private readonly IList<ProgramScheduleItem> _scheduleItems;
private CloneableRandom _random;
private IList<ProgramScheduleItem> _shuffled;
public ShuffledScheduleItemsEnumerator(
IList<ProgramScheduleItem> scheduleItems,
CollectionEnumeratorState state)
{
_scheduleItemsCount = scheduleItems.Count;
_scheduleItems = scheduleItems;
if (state.Index >= _scheduleItems.Count)
{
state.Index = 0;
state.Seed = new Random(state.Seed).Next();
}
_random = new CloneableRandom(state.Seed);
_shuffled = Shuffle(_scheduleItems, _random);
State = new CollectionEnumeratorState { Seed = state.Seed };
while (State.Index < state.Index)
{
MoveNext();
}
}
public CollectionEnumeratorState State { get; }
public ProgramScheduleItem Current => _shuffled[State.Index % _scheduleItemsCount];
public void MoveNext()
{
if ((State.Index + 1) % _scheduleItemsCount == 0)
{
ProgramScheduleItem tail = Current;
State.Index = 0;
do
{
State.Seed = _random.Next();
_random = new CloneableRandom(State.Seed);
_shuffled = Shuffle(_scheduleItems, _random);
} while (_scheduleItems.Count > 1 && Current == tail);
}
else
{
State.Index++;
}
State.Index %= _scheduleItemsCount;
}
public ProgramScheduleItem Peek(int offset)
{
if (offset == 0)
{
return Current;
}
if ((State.Index + offset) % _scheduleItemsCount == 0)
{
IList<ProgramScheduleItem> shuffled;
ProgramScheduleItem tail = Current;
// clone the random
CloneableRandom randomCopy = _random.Clone();
do
{
int newSeed = randomCopy.Next();
randomCopy = new CloneableRandom(newSeed);
shuffled = Shuffle(_scheduleItems, randomCopy);
} while (_scheduleItems.Count > 1 && shuffled[0] == tail);
return shuffled[0];
}
return _shuffled[(State.Index + offset) % _scheduleItemsCount];
}
private IList<ProgramScheduleItem> Shuffle(IEnumerable<ProgramScheduleItem> list, CloneableRandom random)
{
ProgramScheduleItem[] copy = list.ToArray();
int n = copy.Length;
while (n > 1)
{
n--;
int k = random.Next(n + 1);
(copy[k], copy[n]) = (copy[n], copy[k]);
}
return copy;
}
}