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 4 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 @@ -20,11 +20,25 @@ public interface ISchedulingEngine
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);
Task AddCollection(string key, string collectionName, 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
ISchedulingEngine AddCount(
bool AddCount(
string content,
int count,
Option<FillerKind> fillerKind,

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

@ -0,0 +1,8 @@ @@ -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; @@ -2,32 +2,31 @@ using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Extensions;
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(
YamlPlayoutContentMarathonItem marathon,
public async Task<Option<MarathonContentResult>> GetEnumerator(
Dictionary<string, List<string>> guids,
List<string> searches,
string groupBy,
bool shuffleGroups,
PlaybackOrder itemPlaybackOrder,
bool playAllItems,
CollectionEnumeratorState state,
CancellationToken cancellationToken)
{
if (!Enum.TryParse(marathon.ItemOrder, true, out PlaybackOrder playbackOrder))
{
playbackOrder = PlaybackOrder.Shuffle;
}
var allMediaItems = new List<MediaItem>();
// 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]));
}
// grab items from each search
foreach (string query in marathon.Searches)
foreach (string query in searches)
{
allMediaItems.AddRange(await mediaCollectionRepository.GetSmartCollectionItems(query, string.Empty));
}
@ -35,22 +34,22 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio @@ -35,22 +34,22 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
List<IGrouping<GroupKey, MediaItem>> groups = [];
// group by show
if (string.Equals(marathon.GroupBy, "show", StringComparison.OrdinalIgnoreCase))
if (string.Equals(groupBy, "show", StringComparison.OrdinalIgnoreCase))
{
groups.AddRange(allMediaItems.GroupBy(MediaItemKeyByShow));
}
// 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));
}
// 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));
}
// 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));
}
@ -60,7 +59,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio @@ -60,7 +59,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
for (var index = 0; index < groups.Count; 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());
}
@ -68,10 +67,10 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio @@ -68,10 +67,10 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
mediaCollectionRepository,
itemMap,
state,
marathon.ShuffleGroups,
shuffleGroups,
cancellationToken);
return new YamlMarathonContentResult(
return new MarathonContentResult(
enumerator,
itemMap.ToImmutableDictionary(x => CollectionKey.ForPlaylistItem(x.Key), x => x.Value));
}
@ -133,7 +132,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio @@ -133,7 +132,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
private static PlaylistItem GroupToPlaylistItem(
int index,
YamlPlayoutContentMarathonItem marathon,
bool playAllItems,
PlaybackOrder playbackOrder,
IGrouping<GroupKey, MediaItem> group) =>
new()
@ -146,7 +145,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio @@ -146,7 +145,7 @@ public class YamlPlayoutMarathonHelper(IMediaCollectionRepository mediaCollectio
SmartCollectionId = group.Key.SmartCollectionId,
MediaItemId = group.Key.MediaItemId,
PlayAll = marathon.PlayAllItems,
PlayAll = playAllItems,
PlaybackOrder = playbackOrder,
IncludeInProgramGuide = true

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

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
@ -98,7 +99,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -98,7 +99,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
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))
{
@ -107,7 +108,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -107,7 +108,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty collection {Name}", collectionName);
return this;
return;
}
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
@ -128,11 +129,159 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -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))
{
@ -141,7 +290,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -141,7 +290,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty search query {Query}", query);
return this;
return;
}
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
@ -162,11 +311,45 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -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,
int count,
Option<FillerKind> fillerKind,
@ -176,9 +359,11 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -176,9 +359,11 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
if (!_enumerators.TryGetValue(content, out EnumeratorDetails enumeratorDetails))
{
logger.LogWarning("Skipping invalid content {Key}", content);
return this;
return false;
}
var result = false;
for (var i = 0; i < count; i++)
{
// foreach (string preRollSequence in context.GetPreRollSequence())
@ -244,6 +429,8 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -244,6 +429,8 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
}
enumeratorDetails.Enumerator.MoveNext();
result = true;
}
// foreach (string postRollSequence in context.GetPostRollSequence())
@ -254,7 +441,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -254,7 +441,7 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
// }
}
return this;
return result;
}
public ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset)
@ -330,11 +517,10 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -330,11 +517,10 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
return Option<IMediaCollectionEnumerator>.None;
}
private void ApplyHistory(
private void ApplyPlaylistHistory(
string historyKey,
List<MediaItem> collectionItems,
IMediaCollectionEnumerator enumerator,
PlaybackOrder playbackOrder)
ImmutableDictionary<CollectionKey, List<MediaItem>> itemMap,
PlaylistEnumerator playlistEnumerator)
{
DateTime historyTime = _state.CurrentTime.UtcDateTime;
@ -354,106 +540,131 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito @@ -354,106 +540,131 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
.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)
{
Option<PlayoutHistory> maybePrimaryHistory = maybeHistory
.Filter(h => string.IsNullOrWhiteSpace(h.ChildKey))
.HeadOrNone();
var hasSetEnumeratorIndex = false;
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();
foreach ((IMediaCollectionEnumerator childEnumerator, CollectionKey collectionKey) in
playlistEnumerator.ChildEnumerators)
if (!itemMap.TryGetValue(collectionKey, out List<MediaItem> collectionItems) ||
collectionItems.Count == 0)
{
PlaybackOrder itemPlaybackOrder = childEnumerator switch
{
ChronologicalMediaCollectionEnumerator => PlaybackOrder.Chronological,
RandomizedMediaCollectionEnumerator => PlaybackOrder.Random,
ShuffledMediaCollectionEnumerator => PlaybackOrder.Shuffle,
_ => PlaybackOrder.None
};
continue;
}
Option<PlayoutHistory> maybeApplicableHistory = maybeHistory
.Filter(h => h.ChildKey == HistoryDetails.KeyForCollectionKey(collectionKey))
.HeadOrNone();
foreach (PlayoutHistory h in maybeApplicableHistory)
{
// 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(
// "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;
}
// 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();
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
}
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
foreach (PlayoutHistory h in maybeHistory)
{
// logger.LogDebug("History is applicable: {When}: {History}", h.When, h.Details);
if (collectionItems.Count == 0)
{
return;
}
enumerator.ResetState(
new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 });
// 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);
if (playbackOrder is PlaybackOrder.Chronological)
{
HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
enumerator,
playbackOrder);
}
enumerator.ResetState(new CollectionEnumeratorState { Seed = enumerator.State.Seed, Index = h.Index + 1 });
if (playbackOrder is PlaybackOrder.Chronological)
{
HistoryDetails.MoveToNextItem(
collectionItems,
h.Details,
enumerator,
playbackOrder);
}
}
}

17
ErsatzTV.Core/Scheduling/HistoryDetails.cs

@ -54,7 +54,22 @@ internal static class HistoryDetails @@ -54,7 +54,22 @@ internal static class HistoryDetails
var historyKey = new Dictionary<string, object>
{
{ "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);

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

@ -1,34 +1,117 @@ @@ -1,34 +1,117 @@
using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling.Engine;
using IronPython.Runtime;
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
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))
{
return false;
return;
}
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))
{
return false;
return;
}
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; @@ -7,11 +7,19 @@ namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public class PlayoutModule(ISchedulingEngine schedulingEngine)
{
public int FailureCount { get; private set; }
// 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;
if (Enum.TryParse(filler_kind, ignoreCase: true, out FillerKind fillerKind))
@ -19,7 +27,15 @@ public class PlayoutModule(ISchedulingEngine schedulingEngine) @@ -19,7 +27,15 @@ public class PlayoutModule(ISchedulingEngine schedulingEngine)
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( @@ -89,7 +89,7 @@ public class ScriptedPlayoutBuilder(
// define content first
engine.Operations.Invoke(defineContentFunc);
var context = new PythonPlayoutContext(schedulingEngine.GetState(), engine);
var context = new PythonPlayoutContext(schedulingEngine.GetState(), playoutModule, engine);
// reset if applicable
if (mode is PlayoutBuildMode.Reset && resetPlayoutFunc != null)
@ -134,12 +134,18 @@ public class ScriptedPlayoutBuilder( @@ -134,12 +134,18 @@ public class ScriptedPlayoutBuilder(
public class PythonPlayoutContext
{
private readonly ISchedulingEngineState _state;
private readonly PlayoutModule _playoutModule;
private readonly ScriptEngine _scriptEngine;
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;
_playoutModule = playoutModule;
_scriptEngine = scriptEngine;
_datetimeModule = _scriptEngine.ImportModule("datetime");
}
@ -147,7 +153,17 @@ public class ScriptedPlayoutBuilder( @@ -147,7 +153,17 @@ public class ScriptedPlayoutBuilder(
public object current_time => ToPythonDateTime(_state.CurrentTime);
//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)
{

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

@ -2,6 +2,7 @@ using ErsatzTV.Core.Domain; @@ -2,6 +2,7 @@ using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.BlockScheduling;
using ErsatzTV.Core.Scheduling.Engine;
using ErsatzTV.Core.Scheduling.YamlScheduling.Models;
using Microsoft.Extensions.Logging;
@ -96,19 +97,42 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor @@ -96,19 +97,42 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
// marathon is a special case that needs to be handled on its own
if (content is YamlPlayoutContentMarathonItem marathon)
{
var helper = new YamlPlayoutMarathonHelper(mediaCollectionRepository);
Option<YamlMarathonContentResult> maybeResult = await helper.GetEnumerator(
marathon,
var helper = new MarathonHelper(mediaCollectionRepository);
var guids = new Dictionary<string, List<string>>();
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,
cancellationToken);
foreach (YamlMarathonContentResult result in maybeResult)
foreach (MarathonContentResult result in maybeResult)
{
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in result.Content)
{
_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 @@ @@ -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