mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* start to add content to scheduling engine * add first content instruction * add search content * allow scripted schedule creation * don't use scheduling engine in sequential playout builder, yetpull/2339/head
11 changed files with 898 additions and 30 deletions
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.Engine; |
||||
|
||||
public interface ISchedulingEngine |
||||
{ |
||||
ISchedulingEngine WithPlayoutId(int playoutId); |
||||
|
||||
ISchedulingEngine WithMode(PlayoutBuildMode mode); |
||||
|
||||
ISchedulingEngine WithSeed(int seed); |
||||
|
||||
ISchedulingEngine WithReferenceData(PlayoutReferenceData referenceData); |
||||
|
||||
ISchedulingEngine BuildBetween(DateTimeOffset start, DateTimeOffset finish); |
||||
|
||||
ISchedulingEngine RemoveBefore(DateTimeOffset removeBefore); |
||||
|
||||
ISchedulingEngine RestoreOrReset(Option<PlayoutAnchor> maybeAnchor); |
||||
|
||||
// content definitions
|
||||
Task<ISchedulingEngine> AddCollection(string key, string collectionName, PlaybackOrder playbackOrder); |
||||
Task<ISchedulingEngine> AddSearch(string key, string query, PlaybackOrder playbackOrder); |
||||
|
||||
// content instructions
|
||||
ISchedulingEngine AddCount( |
||||
string content, |
||||
int count, |
||||
Option<FillerKind> fillerKind, |
||||
string customTitle, |
||||
bool disableWatermarks); |
||||
|
||||
// control instructions
|
||||
ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset); |
||||
|
||||
|
||||
|
||||
PlayoutAnchor GetAnchor(); |
||||
|
||||
ISchedulingEngineState GetState(); |
||||
} |
||||
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.Engine; |
||||
|
||||
public interface ISchedulingEngineState |
||||
{ |
||||
// state
|
||||
int PlayoutId { get; } |
||||
PlayoutBuildMode Mode { get; } |
||||
int Seed { get; } |
||||
DateTimeOffset CurrentTime { get; } |
||||
DateTimeOffset Finish { get; } |
||||
|
||||
// result
|
||||
Option<DateTimeOffset> RemoveBefore { get; } |
||||
bool ClearItems { get; } |
||||
List<PlayoutItem> AddedItems { get; } |
||||
System.Collections.Generic.HashSet<int> HistoryToRemove { get; } |
||||
List<PlayoutHistory> AddedHistory { get; } |
||||
} |
||||
@ -0,0 +1,655 @@
@@ -0,0 +1,655 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.Extensions; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using ErsatzTV.Core.Scheduling.BlockScheduling; |
||||
using Microsoft.Extensions.Logging; |
||||
using Newtonsoft.Json; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.Engine; |
||||
|
||||
public class SchedulingEngine(IMediaCollectionRepository mediaCollectionRepository, ILogger<SchedulingEngine> logger) |
||||
: ISchedulingEngine |
||||
{ |
||||
private static readonly JsonSerializerSettings JsonSettings = new() |
||||
{ |
||||
NullValueHandling = NullValueHandling.Ignore |
||||
}; |
||||
|
||||
private readonly Dictionary<string, EnumeratorDetails> _enumerators = new(); |
||||
private readonly SchedulingEngineState _state = new(0); |
||||
private PlayoutReferenceData _referenceData; |
||||
|
||||
public ISchedulingEngine WithPlayoutId(int playoutId) |
||||
{ |
||||
_state.PlayoutId = playoutId; |
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine WithMode(PlayoutBuildMode mode) |
||||
{ |
||||
_state.Mode = mode; |
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine WithSeed(int seed) |
||||
{ |
||||
_state.Seed = seed; |
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine WithReferenceData(PlayoutReferenceData referenceData) |
||||
{ |
||||
_referenceData = referenceData; |
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine BuildBetween(DateTimeOffset start, DateTimeOffset finish) |
||||
{ |
||||
_state.Start = start; |
||||
_state.Finish = finish; |
||||
_state.CurrentTime = start; |
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine RemoveBefore(DateTimeOffset removeBefore) |
||||
{ |
||||
_state.RemoveBefore = removeBefore; |
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine RestoreOrReset(Option<PlayoutAnchor> maybeAnchor) |
||||
{ |
||||
if (_state.Mode is PlayoutBuildMode.Reset) |
||||
{ |
||||
// erase items, not history
|
||||
_state.ClearItems = true; |
||||
|
||||
// remove any future or "currently active" history items
|
||||
// this prevents "walking" the playout forward by repeatedly resetting
|
||||
var toRemove = new List<PlayoutHistory>(); |
||||
toRemove.AddRange( |
||||
_referenceData.PlayoutHistory.Filter(h => |
||||
h.When > _state.Start.UtcDateTime || |
||||
h.When <= _state.Start.UtcDateTime && h.Finish >= _state.Start.UtcDateTime)); |
||||
foreach (PlayoutHistory history in toRemove) |
||||
{ |
||||
_state.HistoryToRemove.Add(history.Id); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
foreach (PlayoutAnchor anchor in maybeAnchor) |
||||
{ |
||||
_state.CurrentTime = new DateTimeOffset(anchor.NextStart.ToLocalTime(), _state.CurrentTime.Offset); |
||||
|
||||
if (string.IsNullOrWhiteSpace(anchor.Context)) |
||||
{ |
||||
break; |
||||
} |
||||
|
||||
SerializedState state = JsonConvert.DeserializeObject<SerializedState>(anchor.Context); |
||||
_state.LoadContext(state); |
||||
} |
||||
} |
||||
|
||||
return this; |
||||
} |
||||
|
||||
public async Task<ISchedulingEngine> AddCollection(string key, string collectionName, PlaybackOrder playbackOrder) |
||||
{ |
||||
if (!_enumerators.ContainsKey(key)) |
||||
{ |
||||
int index = _enumerators.Count; |
||||
List<MediaItem> items = await mediaCollectionRepository.GetCollectionItemsByName(collectionName); |
||||
if (items.Count == 0) |
||||
{ |
||||
logger.LogWarning("Skipping invalid or empty collection {Name}", collectionName); |
||||
return this; |
||||
} |
||||
|
||||
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 }; |
||||
foreach (var enumerator in EnumeratorForContent(items, state, playbackOrder)) |
||||
{ |
||||
string historyKey = HistoryDetails.KeyForSchedulingContent(key, playbackOrder); |
||||
var details = new EnumeratorDetails(enumerator, historyKey, playbackOrder); |
||||
|
||||
if (_enumerators.TryAdd(key, details)) |
||||
{ |
||||
logger.LogDebug( |
||||
"Added collection {Name} with key {Key} and order {Order}", |
||||
collectionName, |
||||
key, |
||||
playbackOrder); |
||||
|
||||
ApplyHistory(historyKey, items, enumerator, playbackOrder); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return this; |
||||
} |
||||
|
||||
public async Task<ISchedulingEngine> AddSearch(string key, string query, PlaybackOrder playbackOrder) |
||||
{ |
||||
if (!_enumerators.ContainsKey(key)) |
||||
{ |
||||
int index = _enumerators.Count; |
||||
List<MediaItem> items = await mediaCollectionRepository.GetSmartCollectionItems(query, string.Empty); |
||||
if (items.Count == 0) |
||||
{ |
||||
logger.LogWarning("Skipping invalid or empty search query {Query}", query); |
||||
return this; |
||||
} |
||||
|
||||
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 }; |
||||
foreach (var enumerator in EnumeratorForContent(items, state, playbackOrder)) |
||||
{ |
||||
string historyKey = HistoryDetails.KeyForSchedulingContent(key, playbackOrder); |
||||
var details = new EnumeratorDetails(enumerator, historyKey, playbackOrder); |
||||
|
||||
if (_enumerators.TryAdd(key, details)) |
||||
{ |
||||
logger.LogDebug( |
||||
"Added search query {Query} with key {Key} and order {Order}", |
||||
query, |
||||
key, |
||||
playbackOrder); |
||||
|
||||
ApplyHistory(historyKey, items, enumerator, playbackOrder); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine AddCount( |
||||
string content, |
||||
int count, |
||||
Option<FillerKind> fillerKind, |
||||
string customTitle, |
||||
bool disableWatermarks) |
||||
{ |
||||
if (!_enumerators.TryGetValue(content, out EnumeratorDetails enumeratorDetails)) |
||||
{ |
||||
logger.LogWarning("Skipping invalid content {Key}", content); |
||||
return this; |
||||
} |
||||
|
||||
for (var i = 0; i < count; i++) |
||||
{ |
||||
// foreach (string preRollSequence in context.GetPreRollSequence())
|
||||
// {
|
||||
// context.PushFillerKind(FillerKind.PreRoll);
|
||||
// await executeSequence(preRollSequence);
|
||||
// context.PopFillerKind();
|
||||
// }
|
||||
|
||||
foreach (MediaItem mediaItem in enumeratorDetails.Enumerator.Current) |
||||
{ |
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem); |
||||
|
||||
// create a playout item
|
||||
var playoutItem = new PlayoutItem |
||||
{ |
||||
PlayoutId = _state.PlayoutId, |
||||
MediaItemId = mediaItem.Id, |
||||
Start = _state.CurrentTime.UtcDateTime, |
||||
Finish = _state.CurrentTime.UtcDateTime + itemDuration, |
||||
InPoint = TimeSpan.Zero, |
||||
OutPoint = itemDuration, |
||||
FillerKind = GetFillerKind(fillerKind), |
||||
CustomTitle = string.IsNullOrWhiteSpace(customTitle) ? null : customTitle, |
||||
DisableWatermarks = disableWatermarks, |
||||
GuideGroup = _state.PeekNextGuideGroup(), |
||||
PlayoutItemWatermarks = [], |
||||
PlayoutItemGraphicsElements = [] |
||||
}; |
||||
|
||||
// foreach (int watermarkId in context.GetChannelWatermarkIds())
|
||||
// {
|
||||
// playoutItem.PlayoutItemWatermarks.Add(
|
||||
// new PlayoutItemWatermark
|
||||
// {
|
||||
// PlayoutItem = playoutItem,
|
||||
// WatermarkId = watermarkId
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// foreach ((int graphicsElementId, string variablesJson) in context.GetGraphicsElements())
|
||||
// {
|
||||
// playoutItem.PlayoutItemGraphicsElements.Add(
|
||||
// new PlayoutItemGraphicsElement
|
||||
// {
|
||||
// PlayoutItem = playoutItem,
|
||||
// GraphicsElementId = graphicsElementId,
|
||||
// Variables = variablesJson
|
||||
// });
|
||||
// }
|
||||
|
||||
//await AddItemAndMidRoll(context, playoutItem, mediaItem, executeSequence);
|
||||
|
||||
_state.AddedItems.Add(playoutItem); |
||||
_state.CurrentTime += playoutItem.OutPoint - playoutItem.InPoint; |
||||
_state.AdvanceGuideGroup(); |
||||
|
||||
// create history record
|
||||
List<PlayoutHistory> maybeHistory = GetHistoryForItem(enumeratorDetails, playoutItem, mediaItem); |
||||
foreach (PlayoutHistory history in maybeHistory) |
||||
{ |
||||
_state.AddedHistory.Add(history); |
||||
} |
||||
|
||||
enumeratorDetails.Enumerator.MoveNext(); |
||||
} |
||||
|
||||
// foreach (string postRollSequence in context.GetPostRollSequence())
|
||||
// {
|
||||
// context.PushFillerKind(FillerKind.PostRoll);
|
||||
// await executeSequence(postRollSequence);
|
||||
// context.PopFillerKind();
|
||||
// }
|
||||
} |
||||
|
||||
return this; |
||||
} |
||||
|
||||
public ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset) |
||||
{ |
||||
var currentTime = _state.CurrentTime; |
||||
|
||||
var dayOnly = DateOnly.FromDateTime(currentTime.LocalDateTime); |
||||
var timeOnly = TimeOnly.FromDateTime(currentTime.LocalDateTime); |
||||
|
||||
if (timeOnly > waitUntil) |
||||
{ |
||||
if (tomorrow) |
||||
{ |
||||
// this is wrong when offset changes
|
||||
dayOnly = dayOnly.AddDays(1); |
||||
currentTime = new DateTimeOffset(dayOnly, waitUntil, currentTime.Offset); |
||||
} |
||||
else if (rewindOnReset && _state.Mode == PlayoutBuildMode.Reset) |
||||
{ |
||||
// maybe wrong when offset changes?
|
||||
currentTime = new DateTimeOffset(dayOnly, waitUntil, currentTime.Offset); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
// this is wrong when offset changes
|
||||
currentTime = new DateTimeOffset(dayOnly, waitUntil, currentTime.Offset); |
||||
} |
||||
|
||||
_state.CurrentTime = currentTime; |
||||
|
||||
return this; |
||||
} |
||||
|
||||
public PlayoutAnchor GetAnchor() |
||||
{ |
||||
DateTime maxTime = _state.CurrentTime.UtcDateTime; |
||||
if (_state.AddedItems.Count > 0) |
||||
{ |
||||
maxTime = _state.AddedItems.Max(i => i.Finish); |
||||
} |
||||
|
||||
return new PlayoutAnchor |
||||
{ |
||||
NextStart = maxTime, |
||||
Context = _state.SerializeContext() |
||||
}; |
||||
} |
||||
|
||||
public ISchedulingEngineState GetState() |
||||
{ |
||||
return _state; |
||||
} |
||||
|
||||
private static Option<IMediaCollectionEnumerator> EnumeratorForContent( |
||||
List<MediaItem> items, |
||||
CollectionEnumeratorState state, |
||||
PlaybackOrder playbackOrder, |
||||
bool multiPart = false) |
||||
{ |
||||
switch (playbackOrder) |
||||
{ |
||||
case PlaybackOrder.Chronological: |
||||
return new ChronologicalMediaCollectionEnumerator(items, state); |
||||
case PlaybackOrder.Shuffle: |
||||
bool keepMultiPartEpisodesTogether = multiPart; |
||||
List<GroupedMediaItem> groupedMediaItems = keepMultiPartEpisodesTogether |
||||
? MultiPartEpisodeGrouper.GroupMediaItems(items, false) |
||||
: items.Map(mi => new GroupedMediaItem(mi, null)).ToList(); |
||||
return new BlockPlayoutShuffledMediaCollectionEnumerator(groupedMediaItems, state); |
||||
} |
||||
|
||||
return Option<IMediaCollectionEnumerator>.None; |
||||
} |
||||
|
||||
private void ApplyHistory( |
||||
string historyKey, |
||||
List<MediaItem> collectionItems, |
||||
IMediaCollectionEnumerator enumerator, |
||||
PlaybackOrder playbackOrder) |
||||
{ |
||||
DateTime historyTime = _state.CurrentTime.UtcDateTime; |
||||
|
||||
var filteredHistory = _referenceData.PlayoutHistory.ToList(); |
||||
filteredHistory.RemoveAll(h => _state.HistoryToRemove.Contains(h.Id)); |
||||
|
||||
Option<DateTime> maxWhen = filteredHistory |
||||
.Filter(h => h.Key == historyKey) |
||||
.Filter(h => h.When < historyTime) |
||||
.Map(h => h.When) |
||||
.OrderByDescending(h => h) |
||||
.HeadOrNone() |
||||
.IfNone(DateTime.MinValue); |
||||
|
||||
var maybeHistory = filteredHistory |
||||
.Filter(h => h.Key == historyKey) |
||||
.Filter(h => h.When == maxWhen) |
||||
.ToList(); |
||||
|
||||
if (enumerator is PlaylistEnumerator playlistEnumerator) |
||||
{ |
||||
Option<PlayoutHistory> maybePrimaryHistory = maybeHistory |
||||
.Filter(h => string.IsNullOrWhiteSpace(h.ChildKey)) |
||||
.HeadOrNone(); |
||||
|
||||
foreach (PlayoutHistory primaryHistory in maybePrimaryHistory) |
||||
{ |
||||
var hasSetEnumeratorIndex = false; |
||||
|
||||
var childEnumeratorKeys = playlistEnumerator.ChildEnumerators.Map(x => x.CollectionKey).ToList(); |
||||
foreach ((IMediaCollectionEnumerator childEnumerator, CollectionKey collectionKey) in |
||||
playlistEnumerator.ChildEnumerators) |
||||
{ |
||||
PlaybackOrder itemPlaybackOrder = childEnumerator switch |
||||
{ |
||||
ChronologicalMediaCollectionEnumerator => PlaybackOrder.Chronological, |
||||
RandomizedMediaCollectionEnumerator => PlaybackOrder.Random, |
||||
ShuffledMediaCollectionEnumerator => PlaybackOrder.Shuffle, |
||||
_ => PlaybackOrder.None |
||||
}; |
||||
|
||||
Option<PlayoutHistory> maybeApplicableHistory = maybeHistory |
||||
.Filter(h => h.ChildKey == HistoryDetails.KeyForCollectionKey(collectionKey)) |
||||
.HeadOrNone(); |
||||
|
||||
if (collectionItems.Count == 0) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
foreach (PlayoutHistory h in maybeApplicableHistory) |
||||
{ |
||||
// logger.LogDebug(
|
||||
// "History is applicable: {When}: {ChildKey} / {History} / {IsCurrentChild}",
|
||||
// h.When,
|
||||
// h.ChildKey,
|
||||
// h.Details,
|
||||
// h.IsCurrentChild);
|
||||
|
||||
enumerator.ResetState( |
||||
new CollectionEnumeratorState |
||||
{ |
||||
Seed = enumerator.State.Seed, |
||||
Index = h.Index + (h.IsCurrentChild ? 1 : 0) |
||||
}); |
||||
|
||||
if (itemPlaybackOrder is PlaybackOrder.Chronological) |
||||
{ |
||||
HistoryDetails.MoveToNextItem( |
||||
collectionItems, |
||||
h.Details, |
||||
childEnumerator, |
||||
itemPlaybackOrder, |
||||
true); |
||||
} |
||||
|
||||
if (h.IsCurrentChild) |
||||
{ |
||||
// try to find enumerator based on collection key
|
||||
playlistEnumerator.SetEnumeratorIndex(childEnumeratorKeys.IndexOf(collectionKey)); |
||||
hasSetEnumeratorIndex = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (!hasSetEnumeratorIndex) |
||||
{ |
||||
// falling back to enumerator based on index
|
||||
playlistEnumerator.SetEnumeratorIndex(primaryHistory.Index); |
||||
} |
||||
|
||||
// only move next at the end, because that may also move
|
||||
// the enumerator index
|
||||
playlistEnumerator.MoveNext(); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
if (collectionItems.Count == 0) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory h in maybeHistory) |
||||
{ |
||||
// logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
|
||||
|
||||
enumerator.ResetState( |
||||
new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 }); |
||||
|
||||
if (playbackOrder is PlaybackOrder.Chronological) |
||||
{ |
||||
HistoryDetails.MoveToNextItem( |
||||
collectionItems, |
||||
h.Details, |
||||
enumerator, |
||||
playbackOrder); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static TimeSpan DurationForMediaItem(MediaItem mediaItem) |
||||
{ |
||||
if (mediaItem is Image image) |
||||
{ |
||||
return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); |
||||
} |
||||
|
||||
MediaVersion version = mediaItem.GetHeadVersion(); |
||||
return version.Duration; |
||||
} |
||||
|
||||
private List<PlayoutHistory> GetHistoryForItem( |
||||
EnumeratorDetails enumeratorDetails, |
||||
PlayoutItem playoutItem, |
||||
MediaItem mediaItem) |
||||
{ |
||||
var result = new List<PlayoutHistory>(); |
||||
|
||||
if (enumeratorDetails.Enumerator is PlaylistEnumerator playlistEnumerator) |
||||
{ |
||||
// create a playout history record
|
||||
var nextHistory = new PlayoutHistory |
||||
{ |
||||
PlayoutId = _state.PlayoutId, |
||||
PlaybackOrder = enumeratorDetails.PlaybackOrder, |
||||
Index = playlistEnumerator.EnumeratorIndex, |
||||
When = playoutItem.StartOffset.UtcDateTime, |
||||
Finish = playoutItem.FinishOffset.UtcDateTime, |
||||
Key = enumeratorDetails.HistoryKey, |
||||
Details = HistoryDetails.ForMediaItem(mediaItem) |
||||
}; |
||||
|
||||
result.Add(nextHistory); |
||||
|
||||
for (var i = 0; i < playlistEnumerator.ChildEnumerators.Count; i++) |
||||
{ |
||||
(IMediaCollectionEnumerator childEnumerator, CollectionKey collectionKey) = |
||||
playlistEnumerator.ChildEnumerators[i]; |
||||
bool isCurrentChild = i == playlistEnumerator.EnumeratorIndex; |
||||
foreach (MediaItem currentMediaItem in childEnumerator.Current) |
||||
{ |
||||
// create a playout history record
|
||||
var childHistory = new PlayoutHistory |
||||
{ |
||||
PlayoutId = _state.PlayoutId, |
||||
PlaybackOrder = enumeratorDetails.PlaybackOrder, |
||||
Index = childEnumerator.State.Index, |
||||
When = playoutItem.StartOffset.UtcDateTime, |
||||
Finish = playoutItem.FinishOffset.UtcDateTime, |
||||
Key = enumeratorDetails.HistoryKey, |
||||
ChildKey = HistoryDetails.KeyForCollectionKey(collectionKey), |
||||
IsCurrentChild = isCurrentChild, |
||||
Details = HistoryDetails.ForMediaItem(currentMediaItem) |
||||
}; |
||||
|
||||
result.Add(childHistory); |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
// create a playout history record
|
||||
var nextHistory = new PlayoutHistory |
||||
{ |
||||
PlayoutId = _state.PlayoutId, |
||||
PlaybackOrder = enumeratorDetails.PlaybackOrder, |
||||
Index = enumeratorDetails.Enumerator.State.Index, |
||||
When = playoutItem.StartOffset.UtcDateTime, |
||||
Finish = playoutItem.FinishOffset.UtcDateTime, |
||||
Key = enumeratorDetails.HistoryKey, |
||||
Details = HistoryDetails.ForMediaItem(mediaItem) |
||||
}; |
||||
|
||||
result.Add(nextHistory); |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
protected static FillerKind GetFillerKind(Option<FillerKind> maybeFillerKind) |
||||
{ |
||||
foreach (FillerKind fillerKind in maybeFillerKind) |
||||
{ |
||||
return fillerKind; |
||||
} |
||||
|
||||
// foreach (FillerKind fillerKind in _state.GetFillerKind())
|
||||
// {
|
||||
// return fillerKind;
|
||||
// }
|
||||
|
||||
return FillerKind.None; |
||||
} |
||||
|
||||
public record SerializedState( |
||||
int? GuideGroup, |
||||
bool? GuideGroupLocked); |
||||
|
||||
private class SchedulingEngineState(int guideGroup) : ISchedulingEngineState |
||||
{ |
||||
private int _guideGroup = guideGroup; |
||||
private bool _guideGroupLocked; |
||||
|
||||
// state
|
||||
public int PlayoutId { get; set; } |
||||
public PlayoutBuildMode Mode { get; set; } |
||||
public int Seed { get; set; } |
||||
public DateTimeOffset Finish { get; set; } |
||||
public DateTimeOffset Start { get; set; } |
||||
public DateTimeOffset CurrentTime { get; set; } |
||||
|
||||
// guide group
|
||||
public int PeekNextGuideGroup() |
||||
{ |
||||
if (_guideGroupLocked) |
||||
{ |
||||
return _guideGroup; |
||||
} |
||||
|
||||
int result = _guideGroup + 1; |
||||
if (result > 1000) |
||||
{ |
||||
result = 1; |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
|
||||
public void AdvanceGuideGroup() |
||||
{ |
||||
if (_guideGroupLocked) |
||||
{ |
||||
return; |
||||
} |
||||
|
||||
_guideGroup++; |
||||
if (_guideGroup > 1000) |
||||
{ |
||||
_guideGroup = 1; |
||||
} |
||||
} |
||||
|
||||
public void LockGuideGroup(bool advance = true) |
||||
{ |
||||
if (advance) |
||||
{ |
||||
AdvanceGuideGroup(); |
||||
} |
||||
|
||||
_guideGroupLocked = true; |
||||
} |
||||
|
||||
public void UnlockGuideGroup() => _guideGroupLocked = false; |
||||
|
||||
// result
|
||||
public Option<DateTimeOffset> RemoveBefore { get; set; } |
||||
public bool ClearItems { get; set; } |
||||
public List<PlayoutItem> AddedItems { get; } = []; |
||||
public System.Collections.Generic.HashSet<int> HistoryToRemove { get; } = []; |
||||
public List<PlayoutHistory> AddedHistory { get; } = []; |
||||
|
||||
public string SerializeContext() |
||||
{ |
||||
// string preRollSequence = null;
|
||||
// foreach (string sequence in _preRollSequence)
|
||||
// {
|
||||
// preRollSequence = sequence;
|
||||
// }
|
||||
|
||||
var state = new SerializedState( |
||||
_guideGroup, |
||||
_guideGroupLocked); |
||||
|
||||
return JsonConvert.SerializeObject(state, Formatting.None, JsonSettings); |
||||
} |
||||
|
||||
public void LoadContext(SerializedState state) |
||||
{ |
||||
foreach (int guideGroup in Optional(state.GuideGroup)) |
||||
{ |
||||
_guideGroup = guideGroup; |
||||
} |
||||
|
||||
foreach (bool guideGroupLocked in Optional(state.GuideGroupLocked)) |
||||
{ |
||||
_guideGroupLocked = guideGroupLocked; |
||||
} |
||||
} |
||||
} |
||||
|
||||
private record EnumeratorDetails( |
||||
IMediaCollectionEnumerator Enumerator, |
||||
string HistoryKey, |
||||
PlaybackOrder PlaybackOrder); |
||||
} |
||||
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
using System.Diagnostics.CodeAnalysis; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Scheduling.Engine; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules; |
||||
|
||||
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] |
||||
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")] |
||||
[SuppressMessage("ReSharper", "InconsistentNaming")] |
||||
public class PlayoutModule(ISchedulingEngine schedulingEngine) |
||||
{ |
||||
// content instructions
|
||||
|
||||
public void add_count(string content, int count, string filler_kind = null, string custom_title = null, bool disable_watermarks = false) |
||||
{ |
||||
Option<FillerKind> maybeFillerKind = Option<FillerKind>.None; |
||||
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind)) |
||||
{ |
||||
maybeFillerKind = fillerKind; |
||||
} |
||||
|
||||
schedulingEngine.AddCount(content, count, maybeFillerKind, custom_title, disable_watermarks); |
||||
} |
||||
|
||||
|
||||
// control instructions
|
||||
|
||||
public void wait_until(string when, bool tomorrow = false, bool rewind_on_reset = false) |
||||
{ |
||||
if (TimeOnly.TryParse(when, out TimeOnly waitUntil)) |
||||
{ |
||||
schedulingEngine.WaitUntil(waitUntil, tomorrow, rewind_on_reset); |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue