add option to shuffle schedule items (#652)
* add schedule setting * it works * fix tests * update readme * rebuild all playouts
This commit is contained in:
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user