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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
+20
-21
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
SmartCollectionId = group.Key.SmartCollectionId,
|
||||
MediaItemId = group.Key.MediaItemId,
|
||||
|
||||
PlayAll = marathon.PlayAllItems,
|
||||
PlayAll = playAllItems,
|
||||
PlaybackOrder = playbackOrder,
|
||||
|
||||
IncludeInProgramGuide = true
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async Task<ISchedulingEngine> AddSearch(string key, string query, PlaybackOrder playbackOrder)
|
||||
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 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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ISchedulingEngine AddCount(
|
||||
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 bool AddCount(
|
||||
string content,
|
||||
int count,
|
||||
Option<FillerKind> fillerKind,
|
||||
@@ -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
|
||||
}
|
||||
|
||||
enumeratorDetails.Enumerator.MoveNext();
|
||||
|
||||
result = true;
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -330,6 +517,106 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
|
||||
return Option<IMediaCollectionEnumerator>.None;
|
||||
}
|
||||
|
||||
private void ApplyPlaylistHistory(
|
||||
string historyKey,
|
||||
ImmutableDictionary<CollectionKey, List<MediaItem>> itemMap,
|
||||
PlaylistEnumerator playlistEnumerator)
|
||||
{
|
||||
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();
|
||||
|
||||
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 (!itemMap.TryGetValue(collectionKey, out List<MediaItem> collectionItems) ||
|
||||
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);
|
||||
|
||||
playlistEnumerator.ResetState(
|
||||
new CollectionEnumeratorState
|
||||
{
|
||||
Seed = playlistEnumerator.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();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyHistory(
|
||||
string historyKey,
|
||||
List<MediaItem> collectionItems,
|
||||
@@ -354,106 +641,30 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito
|
||||
.Filter(h => h.When == maxWhen)
|
||||
.ToList();
|
||||
|
||||
if (enumerator is PlaylistEnumerator playlistEnumerator)
|
||||
if (enumerator is 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();
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
if (collectionItems.Count == 0)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
HistoryDetails.MoveToNextItem(
|
||||
collectionItems,
|
||||
h.Details,
|
||||
enumerator,
|
||||
playbackOrder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
return true;
|
||||
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;
|
||||
}
|
||||
|
||||
schedulingEngine
|
||||
.AddShow(key, guids.ToDictionary(k => k.Key.ToString(), k => k.Value.ToString()), playbackOrder)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
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(
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user