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:
Jason Dove
2025-08-24 14:39:34 +00:00
committed by GitHub
parent 61a99c250a
commit 4e2310d008
10 changed files with 535 additions and 158 deletions
@@ -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);
}
}
}