Browse Source

add all content sources to scripted schedules (#2340)

* add show content

* add multi collection content

* add smart collection content

* add playlist content

* fix infinite loop

* add marathon content
pull/2344/head
Jason Dove 11 months ago committed by GitHub
parent
commit
4e2310d008
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 20
      ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs
  2. 8
      ErsatzTV.Core/Scheduling/Engine/MarathonContentResult.cs
  3. 41
      ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs
  4. 397
      ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs
  5. 17
      ErsatzTV.Core/Scheduling/HistoryDetails.cs
  6. 97
      ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/ContentModule.cs
  7. 20
      ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/PlayoutModule.cs
  8. 22
      ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs
  9. 34
      ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs
  10. 9
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlMarathonContentResult.cs

20
ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs

@ -20,11 +20,25 @@ public interface ISchedulingEngine
ISchedulingEngine RestoreOrReset(Option<PlayoutAnchor> maybeAnchor); ISchedulingEngine RestoreOrReset(Option<PlayoutAnchor> maybeAnchor);
// content definitions // content definitions
Task<ISchedulingEngine> AddCollection(string key, string collectionName, PlaybackOrder playbackOrder); Task AddCollection(string key, string collectionName, PlaybackOrder playbackOrder);
Task<ISchedulingEngine> AddSearch(string key, string query, PlaybackOrder playbackOrder);
Task AddMarathon(
string key,
Dictionary<string, List<string>> guids,
List<string> searches,
string groupBy,
bool shuffleGroups,
PlaybackOrder itemPlaybackOrder,
bool playAllItems);
Task AddMultiCollection(string key, string multiCollectionName, PlaybackOrder playbackOrder);
Task AddPlaylist(string key, string playlist, string playlistGroup);
Task AddSmartCollection(string key, string smartCollectionName, PlaybackOrder playbackOrder);
Task AddSearch(string key, string query, PlaybackOrder playbackOrder);
Task AddShow(string key, Dictionary<string, string> guids, PlaybackOrder playbackOrder);
// content instructions // content instructions
ISchedulingEngine AddCount( bool AddCount(
string content, string content,
int count, int count,
Option<FillerKind> fillerKind, Option<FillerKind> fillerKind,

8
ErsatzTV.Core/Scheduling/Engine/MarathonContentResult.cs

@ -0,0 +1,8 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling.Engine;
public record MarathonContentResult(
PlaylistEnumerator PlaylistEnumerator,
ImmutableDictionary<CollectionKey, List<MediaItem>> Content);

41
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutMarathonHelper.cs → ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs

@ -2,32 +2,31 @@ using System.Collections.Immutable;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions; using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
namespace ErsatzTV.Core.Scheduling.YamlScheduling; namespace ErsatzTV.Core.Scheduling.Engine;
public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectionRepository) public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository)
{ {
public async Task<Option<YamlMarathonContentResult>> GetEnumerator( public async Task<Option<MarathonContentResult>> GetEnumerator(
YamlPlayoutContentMarathonItem marathon, Dictionary<string, List<string>> guids,
List<string> searches,
string groupBy,
bool shuffleGroups,
PlaybackOrder itemPlaybackOrder,
bool playAllItems,
CollectionEnumeratorState state, CollectionEnumeratorState state,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
if (!Enum.TryParse(marathon.ItemOrder, true, out PlaybackOrder playbackOrder))
{
playbackOrder = PlaybackOrder.Shuffle;
}
var allMediaItems = new List<MediaItem>(); var allMediaItems = new List<MediaItem>();
// grab items from each show guid // grab items from each show guid
foreach (string showGuid in marathon.Guids.Map(g => $"{g.Source}://{g.Value}")) foreach (string showGuid in guids.SelectMany(g => g.Value.Select(v => $"{g.Key}://{v}")))
{ {
allMediaItems.AddRange(await mediaCollectionRepository.GetShowItemsByShowGuids([showGuid])); allMediaItems.AddRange(await mediaCollectionRepository.GetShowItemsByShowGuids([showGuid]));
} }
// grab items from each search // grab items from each search
foreach (string query in marathon.Searches) foreach (string query in searches)
{ {
allMediaItems.AddRange(await mediaCollectionRepository.GetSmartCollectionItems(query, string.Empty)); allMediaItems.AddRange(await mediaCollectionRepository.GetSmartCollectionItems(query, string.Empty));
} }
@ -35,22 +34,22 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
List<IGrouping<GroupKey, MediaItem>> groups = []; List<IGrouping<GroupKey, MediaItem>> groups = [];
// group by show // group by show
if (string.Equals(marathon.GroupBy, "show", StringComparison.OrdinalIgnoreCase)) if (string.Equals(groupBy, "show", StringComparison.OrdinalIgnoreCase))
{ {
groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByShow)); groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByShow));
} }
// group by season // group by season
else if (string.Equals(marathon.GroupBy, "season", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(groupBy, "season", StringComparison.OrdinalIgnoreCase))
{ {
groups.AddRange(allMediaItems.GroupBy(MediaItemKeyBySeason)); groups.AddRange(allMediaItems.GroupBy(MediaItemKeyBySeason));
} }
// group by artist // group by artist
else if (string.Equals(marathon.GroupBy, "artist", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(groupBy, "artist", StringComparison.OrdinalIgnoreCase))
{ {
groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByArtist)); groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByArtist));
} }
// group by album // group by album
else if (string.Equals(marathon.GroupBy, "album", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(groupBy, "album", StringComparison.OrdinalIgnoreCase))
{ {
groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByAlbum)); groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByAlbum));
} }
@ -60,7 +59,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
for (var index = 0; index < groups.Count; index++) for (var index = 0; index < groups.Count; index++)
{ {
IGrouping<GroupKey, MediaItem> group = groups[index]; IGrouping<GroupKey, MediaItem> group = groups[index];
PlaylistItem playlistItem = GroupToPlaylistItem(index, marathon, playbackOrder, group); PlaylistItem playlistItem = GroupToPlaylistItem(index, playAllItems, itemPlaybackOrder, group);
itemMap.Add(playlistItem, group.ToList()); itemMap.Add(playlistItem, group.ToList());
} }
@ -68,10 +67,10 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
mediaCollectionRepository, mediaCollectionRepository,
itemMap, itemMap,
state, state,
marathon.ShuffleGroups, shuffleGroups,
cancellationToken); cancellationToken);
return new YamlMarathonContentResult( return new MarathonContentResult(
enumerator, enumerator,
itemMap.ToImmutableDictionary(x => CollectionKey.ForPlaylistItem(x.Key), x => x.Value)); itemMap.ToImmutableDictionary(x => CollectionKey.ForPlaylistItem(x.Key), x => x.Value));
} }
@ -133,7 +132,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
private static PlaylistItem GroupToPlaylistItem( private static PlaylistItem GroupToPlaylistItem(
int index, int index,
YamlPlayoutContentMarathonItem marathon, bool playAllItems,
PlaybackOrder playbackOrder, PlaybackOrder playbackOrder,
IGrouping<GroupKey, MediaItem> group) => IGrouping<GroupKey, MediaItem> group) =>
new() new()
@ -146,7 +145,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
SmartCollectionId = group.Key.SmartCollectionId, SmartCollectionId = group.Key.SmartCollectionId,
MediaItemId = group.Key.MediaItemId, MediaItemId = group.Key.MediaItemId,
PlayAll = marathon.PlayAllItems, PlayAll = playAllItems,
PlaybackOrder = playbackOrder, PlaybackOrder = playbackOrder,
IncludeInProgramGuide = true IncludeInProgramGuide = true

397
ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs

@ -1,3 +1,4 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Domain.Scheduling;
@ -98,7 +99,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
return this; return this;
} }
public async Task<ISchedulingEngine> AddCollection(string key, string collectionName, PlaybackOrder playbackOrder) public async Task AddCollection(string key, string collectionName, PlaybackOrder playbackOrder)
{ {
if (!_enumerators.ContainsKey(key)) if (!_enumerators.ContainsKey(key))
{ {
@ -107,7 +108,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
if (items.Count == 0) if (items.Count == 0)
{ {
logger.LogWarning("Skipping invalid or empty collection {Name}", collectionName); logger.LogWarning("Skipping invalid or empty collection {Name}", collectionName);
return this; return;
} }
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 }; var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
@ -128,11 +129,159 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
} }
} }
} }
}
return this; public async Task AddMarathon(
string key,
Dictionary<string, List<string>> guids,
List<string> searches,
string groupBy,
bool shuffleGroups,
PlaybackOrder itemPlaybackOrder,
bool playAllItems)
{
var helper = new MarathonHelper(mediaCollectionRepository);
int index = _enumerators.Count;
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
Option<MarathonContentResult> maybeResult = await helper.GetEnumerator(
guids,
searches,
groupBy,
shuffleGroups,
itemPlaybackOrder,
playAllItems,
state,
CancellationToken.None);
foreach (MarathonContentResult result in maybeResult)
{
foreach (PlaylistEnumerator enumerator in Optional(result.PlaylistEnumerator))
{
string historyKey = HistoryDetails.KeyForSchedulingMarathonContent(key, itemPlaybackOrder, groupBy);
var details = new EnumeratorDetails(enumerator, historyKey, PlaybackOrder.None);
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug("Added marathon with key {Key}", key);
ApplyPlaylistHistory(
historyKey,
result.Content,
enumerator);
}
}
}
await Task.Delay(10);
} }
public async Task<ISchedulingEngine> AddSearch(string key, string query, PlaybackOrder playbackOrder) public async Task AddMultiCollection(
string key,
string multiCollectionName,
PlaybackOrder playbackOrder)
{
if (!_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
List<MediaItem> items = await mediaCollectionRepository.GetMultiCollectionItemsByName(multiCollectionName);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty multi collection {Name}", multiCollectionName);
return;
}
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 multi collection {Name} with key {Key} and order {Order}",
multiCollectionName,
key,
playbackOrder);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
}
}
}
public async Task AddPlaylist(string key, string playlist, string playlistGroup)
{
if (!_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
Dictionary<PlaylistItem, List<MediaItem>> itemMap =
await mediaCollectionRepository.GetPlaylistItemMap(playlistGroup, playlist);
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
var enumerator = await PlaylistEnumerator.Create(
mediaCollectionRepository,
itemMap,
state,
false,
CancellationToken.None);
string historyKey = HistoryDetails.KeyForSchedulingContent(key, PlaybackOrder.None);
var details = new EnumeratorDetails(enumerator, historyKey, PlaybackOrder.None);
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added playlist {Group} / {Name} with key {Key}",
playlistGroup,
playlist,
key);
ApplyPlaylistHistory(
historyKey,
itemMap.ToImmutableDictionary(m => CollectionKey.ForPlaylistItem(m.Key), m => m.Value),
enumerator);
}
}
}
public async Task AddSmartCollection(
string key,
string smartCollectionName,
PlaybackOrder playbackOrder)
{
if (!_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
List<MediaItem> items = await mediaCollectionRepository.GetSmartCollectionItemsByName(smartCollectionName);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty smart collection {Name}", smartCollectionName);
return;
}
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 smart collection {Name} with key {Key} and order {Order}",
smartCollectionName,
key,
playbackOrder);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
}
}
}
public async Task AddSearch(string key, string query, PlaybackOrder playbackOrder)
{ {
if (!_enumerators.ContainsKey(key)) if (!_enumerators.ContainsKey(key))
{ {
@ -141,7 +290,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
if (items.Count == 0) if (items.Count == 0)
{ {
logger.LogWarning("Skipping invalid or empty search query {Query}", query); logger.LogWarning("Skipping invalid or empty search query {Query}", query);
return this; return;
} }
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 }; var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
@ -162,11 +311,45 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
} }
} }
} }
}
return this; public async Task AddShow(
string key,
Dictionary<string, string> guids,
PlaybackOrder playbackOrder)
{
if (!_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetShowItemsByShowGuids(
guids.Map(g => $"{g.Key}://{g.Value}").ToList());
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty show with key {Key}", key);
return;
}
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 show with key {Key} and order {Order}",
key,
playbackOrder);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
}
}
} }
public ISchedulingEngine AddCount( public bool AddCount(
string content, string content,
int count, int count,
Option<FillerKind> fillerKind, Option<FillerKind> fillerKind,
@ -176,9 +359,11 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
if (!_enumerators.TryGetValue(content, out EnumeratorDetails enumeratorDetails)) if (!_enumerators.TryGetValue(content, out EnumeratorDetails enumeratorDetails))
{ {
logger.LogWarning("Skipping invalid content {Key}", content); logger.LogWarning("Skipping invalid content {Key}", content);
return this; return false;
} }
var result = false;
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
// foreach (string preRollSequence in context.GetPreRollSequence()) // foreach (string preRollSequence in context.GetPreRollSequence())
@ -244,6 +429,8 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
} }
enumeratorDetails.Enumerator.MoveNext(); enumeratorDetails.Enumerator.MoveNext();
result = true;
} }
// foreach (string postRollSequence in context.GetPostRollSequence()) // foreach (string postRollSequence in context.GetPostRollSequence())
@ -254,7 +441,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
// } // }
} }
return this; return result;
} }
public ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset) public ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset)
@ -330,11 +517,10 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
return Option<IMediaCollectionEnumerator>.None; return Option<IMediaCollectionEnumerator>.None;
} }
private void ApplyHistory( private void ApplyPlaylistHistory(
string historyKey, string historyKey,
List<MediaItem> collectionItems, ImmutableDictionary<CollectionKey, List<MediaItem>> itemMap,
IMediaCollectionEnumerator enumerator, PlaylistEnumerator playlistEnumerator)
PlaybackOrder playbackOrder)
{ {
DateTime historyTime = _state.CurrentTime.UtcDateTime; DateTime historyTime = _state.CurrentTime.UtcDateTime;
@ -354,106 +540,131 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
.Filter(h => h.When == maxWhen) .Filter(h => h.When == maxWhen)
.ToList(); .ToList();
if (enumerator is PlaylistEnumerator playlistEnumerator) Option<PlayoutHistory> maybePrimaryHistory = maybeHistory
.Filter(h => string.IsNullOrWhiteSpace(h.ChildKey))
.HeadOrNone();
foreach (PlayoutHistory primaryHistory in maybePrimaryHistory)
{ {
Option<PlayoutHistory> maybePrimaryHistory = maybeHistory var hasSetEnumeratorIndex = false;
.Filter(h => string.IsNullOrWhiteSpace(h.ChildKey))
.HeadOrNone();
foreach (PlayoutHistory primaryHistory in maybePrimaryHistory) var childEnumeratorKeys = playlistEnumerator.ChildEnumerators.Map(x => x.CollectionKey).ToList();
foreach ((IMediaCollectionEnumerator childEnumerator, CollectionKey collectionKey) in
playlistEnumerator.ChildEnumerators)
{ {
var hasSetEnumeratorIndex = false; 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();
var childEnumeratorKeys = playlistEnumerator.ChildEnumerators.Map(x => x.CollectionKey).ToList(); if (!itemMap.TryGetValue(collectionKey, out List<MediaItem> collectionItems) ||
foreach ((IMediaCollectionEnumerator childEnumerator, CollectionKey collectionKey) in collectionItems.Count == 0)
playlistEnumerator.ChildEnumerators)
{ {
PlaybackOrder itemPlaybackOrder = childEnumerator switch continue;
{ }
ChronologicalMediaCollectionEnumerator => PlaybackOrder.Chronological,
RandomizedMediaCollectionEnumerator => PlaybackOrder.Random,
ShuffledMediaCollectionEnumerator => PlaybackOrder.Shuffle,
_ => PlaybackOrder.None
};
Option<PlayoutHistory> maybeApplicableHistory = maybeHistory foreach (PlayoutHistory h in maybeApplicableHistory)
.Filter(h => h.ChildKey == HistoryDetails.KeyForCollectionKey(collectionKey)) {
.HeadOrNone(); // logger.LogDebug(
// "History is applicable: {When}: {ChildKey} / {History} / {IsCurrentChild}",
// h.When,
// h.ChildKey,
// h.Details,
// h.IsCurrentChild);
playlistEnumerator.ResetState(
new CollectionEnumeratorState
{
Seed = playlistEnumerator.State.Seed,
Index = h.Index + (h.IsCurrentChild ? 1 : 0)
});
if (collectionItems.Count == 0) if (itemPlaybackOrder is PlaybackOrder.Chronological)
{ {
continue; HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
childEnumerator,
itemPlaybackOrder,
true);
} }
foreach (PlayoutHistory h in maybeApplicableHistory) if (h.IsCurrentChild)
{ {
// logger.LogDebug( // try to find enumerator based on collection key
// "History is applicable: {When}: {ChildKey} / {History} / {IsCurrentChild}", playlistEnumerator.SetEnumeratorIndex(childEnumeratorKeys.IndexOf(collectionKey));
// h.When, hasSetEnumeratorIndex = true;
// 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) if (!hasSetEnumeratorIndex)
{ {
// falling back to enumerator based on index // falling back to enumerator based on index
playlistEnumerator.SetEnumeratorIndex(primaryHistory.Index); playlistEnumerator.SetEnumeratorIndex(primaryHistory.Index);
}
// only move next at the end, because that may also move
// the enumerator index
playlistEnumerator.MoveNext();
} }
// only move next at the end, because that may also move
// the enumerator index
playlistEnumerator.MoveNext();
} }
else }
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)
{ {
if (collectionItems.Count == 0) return;
{ }
return;
}
// seek to the appropriate place in the collection enumerator if (collectionItems.Count == 0)
foreach (PlayoutHistory h in maybeHistory) {
{ return;
// logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details); }
enumerator.ResetState( // seek to the appropriate place in the collection enumerator
new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 }); foreach (PlayoutHistory h in maybeHistory)
{
// logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
if (playbackOrder is PlaybackOrder.Chronological) enumerator.ResetState(new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 });
{
HistoryDetails.MoveToNextItem( if (playbackOrder is PlaybackOrder.Chronological)
collectionItems, {
h.Details, HistoryDetails.MoveToNextItem(
enumerator, collectionItems,
playbackOrder); h.Details,
} enumerator,
playbackOrder);
} }
} }
} }

17
ErsatzTV.Core/Scheduling/HistoryDetails.cs

@ -54,7 +54,22 @@ internal static class HistoryDetails
var historyKey = new Dictionary<string, object> var historyKey = new Dictionary<string, object>
{ {
{ "Key", key }, { "Key", key },
{ "Order", playbackOrder.ToString().ToLowerInvariant() } { "Order", playbackOrder.ToString() }
};
return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings);
}
public static string KeyForSchedulingMarathonContent(string key, PlaybackOrder itemPlaybackOrder, string groupBy)
{
var historyKey = new Dictionary<string, object>
{
{ "Key", key },
{ "Order", nameof(PlaybackOrder.None) },
// we need to ignore history when any of these properties change
{ "ItemOrder", itemPlaybackOrder.ToString() },
{ "GroupBy", groupBy }
}; };
return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings); return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings);

97
ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/ContentModule.cs

@ -1,34 +1,117 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling.Engine; using ErsatzTV.Core.Scheduling.Engine;
using IronPython.Runtime;
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules; namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] [SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")] [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public class ContentModule(ISchedulingEngine schedulingEngine) public class ContentModule(ISchedulingEngine schedulingEngine)
{ {
public bool add_search(string key, string query, string order) public void add_search(string key, string query, string order)
{ {
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder)) if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder))
{ {
return false; return;
} }
schedulingEngine.AddSearch(key, query, playbackOrder).GetAwaiter().GetResult(); schedulingEngine.AddSearch(key, query, playbackOrder).GetAwaiter().GetResult();
return true;
} }
public bool add_collection(string key, string collection, string order) public void add_collection(string key, string collection, string order)
{ {
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder)) if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder))
{ {
return false; return;
} }
schedulingEngine.AddCollection(key, collection, playbackOrder).GetAwaiter().GetResult(); schedulingEngine.AddCollection(key, collection, playbackOrder).GetAwaiter().GetResult();
}
public void add_marathon(
string key,
string group_by,
string item_order = "shuffle",
PythonDictionary guids = null,
PythonList searches = null,
bool play_all_items = false,
bool shuffle_groups = false)
{
if (!Enum.TryParse(item_order, ignoreCase: true, out PlaybackOrder itemPlaybackOrder))
{
itemPlaybackOrder = PlaybackOrder.Shuffle;
}
var mappedGuids = new Dictionary<string, List<string>>();
if (guids != null)
{
foreach (KeyValuePair<object, object> guid in guids)
{
var guidKey = guid.Key.ToString();
if (guidKey is not null && guid.Value is PythonList guidValues)
{
mappedGuids.Add(guidKey, guidValues.Select(x => x.ToString()).ToList());
}
}
}
var mappedSearches = new List<string>();
if (searches != null)
{
mappedSearches.AddRange(searches.Select(x => x.ToString()));
}
// guids OR searches are required
if (mappedGuids.Count == 0 && mappedSearches.Count == 0)
{
return;
}
schedulingEngine
.AddMarathon(key, mappedGuids, mappedSearches, group_by, shuffle_groups, itemPlaybackOrder, play_all_items)
.GetAwaiter()
.GetResult();
}
public void add_multi_collection(string key, string multi_collection, string order)
{
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder))
{
return;
}
schedulingEngine.AddMultiCollection(key, multi_collection, playbackOrder).GetAwaiter().GetResult();
}
public void add_playlist(string key, string playlist, string playlist_group)
{
schedulingEngine.AddPlaylist(key, playlist, playlist_group).GetAwaiter().GetResult();
}
public void add_smart_collection(string key, string smart_collection, string order)
{
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder))
{
return;
}
schedulingEngine.AddSmartCollection(key, smart_collection, playbackOrder).GetAwaiter().GetResult();
}
public void add_show(string key, PythonDictionary guids, string order)
{
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder))
{
return;
}
return true; schedulingEngine
.AddShow(key, guids.ToDictionary(k => k.Key.ToString(), k => k.Value.ToString()), playbackOrder)
.GetAwaiter()
.GetResult();
} }
} }

20
ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/PlayoutModule.cs

@ -7,11 +7,19 @@ namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")] [SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")] [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")]
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public class PlayoutModule(ISchedulingEngine schedulingEngine) public class PlayoutModule(ISchedulingEngine schedulingEngine)
{ {
public int FailureCount { get; private set; }
// content instructions // content instructions
public void add_count(string content, int count, string filler_kind = null, string custom_title = null, bool disable_watermarks = false) 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; Option<FillerKind> maybeFillerKind = Option<FillerKind>.None;
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind)) if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind))
@ -19,7 +27,15 @@ public class PlayoutModule(ISchedulingEngine schedulingEngine)
maybeFillerKind = fillerKind; maybeFillerKind = fillerKind;
} }
schedulingEngine.AddCount(content, count, maybeFillerKind, custom_title, disable_watermarks); bool success = schedulingEngine.AddCount(content, count, maybeFillerKind, custom_title, disable_watermarks);
if (success)
{
FailureCount = 0;
}
else
{
FailureCount++;
}
} }

22
ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs

@ -89,7 +89,7 @@ public class ScriptedPlayoutBuilder(
// define content first // define content first
engine.Operations.Invoke(defineContentFunc); engine.Operations.Invoke(defineContentFunc);
var context = new PythonPlayoutContext(schedulingEngine.GetState(), engine); var context = new PythonPlayoutContext(schedulingEngine.GetState(), playoutModule, engine);
// reset if applicable // reset if applicable
if (mode is PlayoutBuildMode.Reset && resetPlayoutFunc != null) if (mode is PlayoutBuildMode.Reset && resetPlayoutFunc != null)
@ -134,12 +134,18 @@ public class ScriptedPlayoutBuilder(
public class PythonPlayoutContext public class PythonPlayoutContext
{ {
private readonly ISchedulingEngineState _state; private readonly ISchedulingEngineState _state;
private readonly PlayoutModule _playoutModule;
private readonly ScriptEngine _scriptEngine; private readonly ScriptEngine _scriptEngine;
private readonly dynamic _datetimeModule; private readonly dynamic _datetimeModule;
private const int MaxFailures = 10;
public PythonPlayoutContext(ISchedulingEngineState state, ScriptEngine scriptEngine) public PythonPlayoutContext(
ISchedulingEngineState state,
PlayoutModule playoutModule,
ScriptEngine scriptEngine)
{ {
_state = state; _state = state;
_playoutModule = playoutModule;
_scriptEngine = scriptEngine; _scriptEngine = scriptEngine;
_datetimeModule = _scriptEngine.ImportModule("datetime"); _datetimeModule = _scriptEngine.ImportModule("datetime");
} }
@ -147,7 +153,17 @@ public class ScriptedPlayoutBuilder(
public object current_time => ToPythonDateTime(_state.CurrentTime); public object current_time => ToPythonDateTime(_state.CurrentTime);
//public object finish => ToPythonDateTime(_state.Finish); //public object finish => ToPythonDateTime(_state.Finish);
public bool is_done() => _state.CurrentTime >= _state.Finish; public bool is_done()
{
if (_playoutModule.FailureCount >= MaxFailures)
{
throw new InvalidOperationException(
$"Script execution halted after {MaxFailures} consecutive failures to add content."
);
}
return _state.CurrentTime >= _state.Finish;
}
private object ToPythonDateTime(DateTimeOffset dto) private object ToPythonDateTime(DateTimeOffset dto)
{ {

34
ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs

@ -2,6 +2,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.BlockScheduling; using ErsatzTV.Core.Scheduling.BlockScheduling;
using ErsatzTV.Core.Scheduling.Engine;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -96,19 +97,42 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
// marathon is a special case that needs to be handled on its own // marathon is a special case that needs to be handled on its own
if (content is YamlPlayoutContentMarathonItem marathon) if (content is YamlPlayoutContentMarathonItem marathon)
{ {
var helper = new YamlPlayoutMarathonHelper(mediaCollectionRepository); var helper = new MarathonHelper(mediaCollectionRepository);
Option<YamlMarathonContentResult> maybeResult = await helper.GetEnumerator( var guids = new Dictionary<string, List<string>>();
marathon, foreach (var guid in marathon.Guids)
{
if (!guids.TryGetValue(guid.Source, out List<string> value))
{
value = [];
guids.Add(guid.Source, value);
}
value.Add(guid.Value);
}
if (!Enum.TryParse(marathon.ItemOrder, true, out PlaybackOrder itemPlaybackOrder))
{
itemPlaybackOrder = PlaybackOrder.Shuffle;
}
Option<MarathonContentResult> maybeResult = await helper.GetEnumerator(
guids,
marathon.Searches,
marathon.GroupBy,
marathon.ShuffleGroups,
itemPlaybackOrder,
marathon.PlayAllItems,
state, state,
cancellationToken); cancellationToken);
foreach (YamlMarathonContentResult result in maybeResult)
foreach (MarathonContentResult result in maybeResult)
{ {
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in result.Content) foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in result.Content)
{ {
_playlistMediaItems.Add(new PlaylistKey(contentKey, collectionKey), mediaItems); _playlistMediaItems.Add(new PlaylistKey(contentKey, collectionKey), mediaItems);
} }
return Some(result.PlaylistEnumerator); return result.PlaylistEnumerator;
} }
} }

9
ErsatzTV.Core/Scheduling/YamlScheduling/YamlMarathonContentResult.cs

@ -1,9 +0,0 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
public record YamlMarathonContentResult(
IMediaCollectionEnumerator PlaylistEnumerator,
ImmutableDictionary<CollectionKey, List<MediaItem>> Content);
Loading…
Cancel
Save