Browse Source

add create_playlist, pre_roll_on, pre_roll_off to scripted schedules (#2387)

pull/2388/head
Jason Dove 4 months ago committed by GitHub
parent
commit
ef5de99f9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      ErsatzTV.Core/Api/ScriptedPlayout/ContentCreatePlaylist.cs
  2. 9
      ErsatzTV.Core/Api/ScriptedPlayout/ControlPreRollOn.cs
  3. 11
      ErsatzTV.Core/Api/ScriptedPlayout/PlaylistItem.cs
  4. 3
      ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs
  5. 5
      ErsatzTV.Core/Scheduling/CollectionKey.cs
  6. 9
      ErsatzTV.Core/Scheduling/Engine/EnumeratorDetails.cs
  7. 4
      ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs
  8. 4
      ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs
  9. 2
      ErsatzTV.Core/Scheduling/Engine/PlaylistContentResult.cs
  10. 66
      ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs
  11. 441
      ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs
  12. 11
      ErsatzTV.Core/Scheduling/HistoryDetails.cs
  13. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs
  14. 53
      ErsatzTV/Controllers/Api/ScriptedScheduleController.cs
  15. 167
      ErsatzTV/wwwroot/openapi/scripted-schedule-tagged.json
  16. 167
      ErsatzTV/wwwroot/openapi/scripted-schedule.json

12
ErsatzTV.Core/Api/ScriptedPlayout/ContentCreatePlaylist.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using System.ComponentModel;
namespace ErsatzTV.Core.Api.ScriptedPlayout;
public record ContentCreatePlaylist
{
[Description("Unique name used to reference this content throughout the scripted schedule")]
public string Key { get; set; }
[Description("List of playlist items")]
public List<PlaylistItem> Items { get; set; }
}

9
ErsatzTV.Core/Api/ScriptedPlayout/ControlPreRollOn.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using System.ComponentModel;
namespace ErsatzTV.Core.Api.ScriptedPlayout;
public record ControlPreRollOn
{
[Description("The 'key' for the scripted playlist")]
public string Playlist { get; set; }
}

11
ErsatzTV.Core/Api/ScriptedPlayout/PlaylistItem.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
using System.ComponentModel;
namespace ErsatzTV.Core.Api.ScriptedPlayout;
public record PlaylistItem
{
[Description("The 'key' for the content")]
public string Content { get; set; }
public int Count { get; set; }
}

3
ErsatzTV.Core/Domain/ProgramScheduleItemCollectionType.cs

@ -18,5 +18,6 @@ public enum ProgramScheduleItemCollectionType @@ -18,5 +18,6 @@ public enum ProgramScheduleItemCollectionType
Image = 60,
RemoteStream = 70,
FakeCollection = 100
FakeCollection = 100,
FakePlaylistItem = 101
}

5
ErsatzTV.Core/Scheduling/CollectionKey.cs

@ -51,6 +51,11 @@ public class CollectionKey : Record<CollectionKey> @@ -51,6 +51,11 @@ public class CollectionKey : Record<CollectionKey>
{
CollectionType = item.CollectionType
},
ProgramScheduleItemCollectionType.FakePlaylistItem => new CollectionKey
{
CollectionType = item.CollectionType,
CollectionId = item.CollectionId
},
ProgramScheduleItemCollectionType.Movie => new CollectionKey
{
CollectionType = item.CollectionType,

9
ErsatzTV.Core/Scheduling/Engine/EnumeratorDetails.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
namespace ErsatzTV.Core.Scheduling.Engine;
public record EnumeratorDetails(
IMediaCollectionEnumerator Enumerator,
string HistoryKey,
PlaybackOrder PlaybackOrder);

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

@ -42,6 +42,7 @@ public interface ISchedulingEngine @@ -42,6 +42,7 @@ public interface ISchedulingEngine
CancellationToken cancellationToken);
Task AddPlaylist(string key, string playlist, string playlistGroup, CancellationToken cancellationToken);
Task CreatePlaylist(string key, Dictionary<string, int> playlistItems, CancellationToken cancellationToken);
Task AddSmartCollection(
string key,
@ -129,6 +130,9 @@ public interface ISchedulingEngine @@ -129,6 +130,9 @@ public interface ISchedulingEngine
Task GraphicsOff(List<string> graphicsElements, CancellationToken cancellationToken);
Task WatermarkOn(List<string> watermarks);
Task WatermarkOff(List<string> watermarks);
void PreRollOn(string content);
void PreRollOff();
void SkipItems(string content, int count);
void SkipToItem(string content, int season, int episode);
ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset);

4
ErsatzTV.Core/Scheduling/Engine/MarathonHelper.cs

@ -7,7 +7,7 @@ namespace ErsatzTV.Core.Scheduling.Engine; @@ -7,7 +7,7 @@ namespace ErsatzTV.Core.Scheduling.Engine;
public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository)
{
public async Task<Option<MarathonContentResult>> GetEnumerator(
public async Task<Option<PlaylistContentResult>> GetEnumerator(
Dictionary<string, List<string>> guids,
List<string> searches,
string groupBy,
@ -70,7 +70,7 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository @@ -70,7 +70,7 @@ public class MarathonHelper(IMediaCollectionRepository mediaCollectionRepository
shuffleGroups,
cancellationToken);
return new MarathonContentResult(
return new PlaylistContentResult(
enumerator,
itemMap.ToImmutableDictionary(x => CollectionKey.ForPlaylistItem(x.Key), x => x.Value));
}

2
ErsatzTV.Core/Scheduling/Engine/MarathonContentResult.cs → ErsatzTV.Core/Scheduling/Engine/PlaylistContentResult.cs

@ -3,6 +3,6 @@ using ErsatzTV.Core.Domain; @@ -3,6 +3,6 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Core.Scheduling.Engine;
public record MarathonContentResult(
public record PlaylistContentResult(
PlaylistEnumerator PlaylistEnumerator,
ImmutableDictionary<CollectionKey, List<MediaItem>> Content);

66
ErsatzTV.Core/Scheduling/Engine/PlaylistHelper.cs

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
using System.Collections.Immutable;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Repositories;
namespace ErsatzTV.Core.Scheduling.Engine;
public class PlaylistHelper(IMediaCollectionRepository mediaCollectionRepository)
{
public async Task<Option<PlaylistContentResult>> GetEnumerator(
Dictionary<string, EnumeratorDetails> enumerators,
Dictionary<string, ImmutableList<MediaItem>> enumeratorMediaItems,
Dictionary<string, int> playlistItems,
CollectionEnumeratorState state,
CancellationToken cancellationToken)
{
Dictionary<PlaylistItem, List<MediaItem>> itemMap = [];
int playlistIndex = 0;
var allKeys = playlistItems.Keys.ToList();
for (var index = 0; index < allKeys.Count; index++)
{
string key = allKeys[index];
ImmutableList<MediaItem> mediaItems = null;
if (!enumerators.TryGetValue(key, out EnumeratorDetails enumeratorDetails) ||
!enumeratorMediaItems.TryGetValue(key, out mediaItems))
{
Console.WriteLine($"Something is wrong with the playlist with key {key}");
Console.WriteLine($"details: {(enumeratorDetails is null ? "null" : "not null")}");
Console.WriteLine($"items: {(mediaItems?.Count ?? -1)}");
continue;
}
int count = playlistItems[key];
for (var i = 0; i < count; i++)
{
PlaylistItem playlistItem = new()
{
Index = playlistIndex,
CollectionType = ProgramScheduleItemCollectionType.FakePlaylistItem,
CollectionId = playlistIndex,
PlayAll = false,
PlaybackOrder = enumeratorDetails.PlaybackOrder,
IncludeInProgramGuide = true
};
itemMap.Add(playlistItem, mediaItems.ToList());
playlistIndex++;
}
}
PlaylistEnumerator enumerator = await PlaylistEnumerator.Create(
mediaCollectionRepository,
itemMap,
state,
shufflePlaylistItems: false,
cancellationToken);
return new PlaylistContentResult(
enumerator,
itemMap.ToImmutableDictionary(x => CollectionKey.ForPlaylistItem(x.Key), x => x.Value));
}
}

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

@ -27,6 +27,7 @@ public class SchedulingEngine( @@ -27,6 +27,7 @@ public class SchedulingEngine(
private readonly Dictionary<string, Option<GraphicsElement>> _graphicsElementCache = new();
private readonly Dictionary<string, Option<ChannelWatermark>> _watermarkCache = new();
private readonly Dictionary<string, EnumeratorDetails> _enumerators = new();
private readonly Dictionary<string, ImmutableList<MediaItem>> _enumeratorMediaItems = new();
private readonly SchedulingEngineState _state = new(0);
private PlayoutReferenceData _referenceData;
@ -112,33 +113,36 @@ public class SchedulingEngine( @@ -112,33 +113,36 @@ public class SchedulingEngine(
PlaybackOrder playbackOrder,
CancellationToken cancellationToken)
{
if (!_enumerators.ContainsKey(key))
if (_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetCollectionItemsByName(collectionName, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty collection {Name}", collectionName);
return;
}
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);
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetCollectionItemsByName(collectionName, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty collection {Name}", collectionName);
return;
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added collection {Name} with key {Key} and order {Order}",
collectionName,
key,
playbackOrder);
_enumeratorMediaItems[key] = items.ToImmutableList();
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);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added collection {Name} with key {Key} and order {Order}",
collectionName,
key,
playbackOrder);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
}
}
@ -152,11 +156,16 @@ public class SchedulingEngine( @@ -152,11 +156,16 @@ public class SchedulingEngine(
PlaybackOrder itemPlaybackOrder,
bool playAllItems)
{
if (_enumerators.ContainsKey(key))
{
return;
}
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(
Option<PlaylistContentResult> maybeResult = await helper.GetEnumerator(
guids,
searches,
groupBy,
@ -166,7 +175,7 @@ public class SchedulingEngine( @@ -166,7 +175,7 @@ public class SchedulingEngine(
state,
CancellationToken.None);
foreach (MarathonContentResult result in maybeResult)
foreach (PlaylistContentResult result in maybeResult)
{
foreach (PlaylistEnumerator enumerator in Optional(result.PlaylistEnumerator))
{
@ -183,8 +192,6 @@ public class SchedulingEngine( @@ -183,8 +192,6 @@ public class SchedulingEngine(
}
}
}
await Task.Delay(10);
}
public async Task AddMultiCollection(
@ -193,33 +200,36 @@ public class SchedulingEngine( @@ -193,33 +200,36 @@ public class SchedulingEngine(
PlaybackOrder playbackOrder,
CancellationToken cancellationToken)
{
if (!_enumerators.ContainsKey(key))
if (_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetMultiCollectionItemsByName(multiCollectionName, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty multi collection {Name}", multiCollectionName);
return;
}
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);
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetMultiCollectionItemsByName(multiCollectionName, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty multi collection {Name}", multiCollectionName);
return;
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added multi collection {Name} with key {Key} and order {Order}",
multiCollectionName,
key,
playbackOrder);
_enumeratorMediaItems[key] = items.ToImmutableList();
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);
ApplyHistory(historyKey, items, enumerator, 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);
}
}
}
@ -230,36 +240,78 @@ public class SchedulingEngine( @@ -230,36 +240,78 @@ public class SchedulingEngine(
string playlistGroup,
CancellationToken cancellationToken)
{
if (!_enumerators.ContainsKey(key))
if (_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
Dictionary<PlaylistItem, List<MediaItem>> itemMap =
await mediaCollectionRepository.GetPlaylistItemMap(playlistGroup, playlist, cancellationToken);
return;
}
int index = _enumerators.Count;
Dictionary<PlaylistItem, List<MediaItem>> itemMap =
await mediaCollectionRepository.GetPlaylistItemMap(playlistGroup, playlist, cancellationToken);
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
var enumerator = await PlaylistEnumerator.Create(
mediaCollectionRepository,
itemMap,
state,
false,
CancellationToken.None);
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);
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);
if (_enumerators.TryAdd(key, details))
ApplyPlaylistHistory(
historyKey,
itemMap.ToImmutableDictionary(m => CollectionKey.ForPlaylistItem(m.Key), m => m.Value),
enumerator);
}
}
public async Task CreatePlaylist(
string key,
Dictionary<string, int> playlistItems,
CancellationToken cancellationToken)
{
if (_enumerators.ContainsKey(key))
{
return;
}
var helper = new PlaylistHelper(mediaCollectionRepository);
int index = _enumerators.Count;
var state = new CollectionEnumeratorState { Seed = _state.Seed + index, Index = 0 };
Option<PlaylistContentResult> maybeResult = await helper.GetEnumerator(
_enumerators,
_enumeratorMediaItems,
playlistItems,
state,
CancellationToken.None);
foreach (PlaylistContentResult result in maybeResult)
{
foreach (PlaylistEnumerator enumerator in Optional(result.PlaylistEnumerator))
{
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);
string historyKey = HistoryDetails.KeyForSchedulingPlaylistContent(key);
var details = new EnumeratorDetails(enumerator, historyKey, PlaybackOrder.None);
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug("Created playlist with key {Key}", key);
ApplyPlaylistHistory(
historyKey,
result.Content,
enumerator);
}
}
}
}
@ -270,33 +322,36 @@ public class SchedulingEngine( @@ -270,33 +322,36 @@ public class SchedulingEngine(
PlaybackOrder playbackOrder,
CancellationToken cancellationToken)
{
if (!_enumerators.ContainsKey(key))
if (_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetSmartCollectionItemsByName(smartCollectionName, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty smart collection {Name}", smartCollectionName);
return;
}
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);
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetSmartCollectionItemsByName(smartCollectionName, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty smart collection {Name}", smartCollectionName);
return;
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added smart collection {Name} with key {Key} and order {Order}",
smartCollectionName,
key,
playbackOrder);
_enumeratorMediaItems[key] = items.ToImmutableList();
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);
ApplyHistory(historyKey, items, enumerator, 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);
}
}
}
@ -307,33 +362,36 @@ public class SchedulingEngine( @@ -307,33 +362,36 @@ public class SchedulingEngine(
PlaybackOrder playbackOrder,
CancellationToken cancellationToken)
{
if (!_enumerators.ContainsKey(key))
if (_enumerators.ContainsKey(key))
{
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetSmartCollectionItems(query, string.Empty, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty search query {Query}", query);
return;
}
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);
int index = _enumerators.Count;
List<MediaItem> items =
await mediaCollectionRepository.GetSmartCollectionItems(query, string.Empty, cancellationToken);
if (items.Count == 0)
{
logger.LogWarning("Skipping invalid or empty search query {Query}", query);
return;
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added search query {Query} with key {Key} and order {Order}",
query,
key,
playbackOrder);
_enumeratorMediaItems[key] = items.ToImmutableList();
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);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added search query {Query} with key {Key} and order {Order}",
query,
key,
playbackOrder);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
}
}
@ -343,33 +401,36 @@ public class SchedulingEngine( @@ -343,33 +401,36 @@ public class SchedulingEngine(
Dictionary<string, string> guids,
PlaybackOrder playbackOrder)
{
if (!_enumerators.ContainsKey(key))
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;
}
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);
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;
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added show with key {Key} and order {Order}",
key,
playbackOrder);
_enumeratorMediaItems[key] = items.ToImmutableList();
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);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
if (_enumerators.TryAdd(key, details))
{
logger.LogDebug(
"Added show with key {Key} and order {Order}",
key,
playbackOrder);
ApplyHistory(historyKey, items, enumerator, playbackOrder);
}
}
}
@ -648,18 +709,15 @@ public class SchedulingEngine( @@ -648,18 +709,15 @@ public class SchedulingEngine(
TimeSpan remainingToFill = targetTime - _state.CurrentTime;
while (!done && enumeratorDetails.Enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero)
{
// foreach (string preRollSequence in context.GetPreRollSequence())
// {
// context.PushFillerKind(FillerKind.PreRoll);
// await executeSequence(preRollSequence);
// context.PopFillerKind();
//
// remainingToFill = targetTime - context.CurrentTime;
// if (remainingToFill <= TimeSpan.Zero)
// {
// break;
// }
// }
foreach (string preRollPlaylist in _state.GetPreRollPlaylist())
{
AddFillerPlaylist(preRollPlaylist, FillerKind.PreRoll);
remainingToFill = targetTime - _state.CurrentTime;
if (remainingToFill <= TimeSpan.Zero)
{
break;
}
}
foreach (MediaItem mediaItem in enumeratorDetails.Enumerator.Current)
{
@ -801,23 +859,46 @@ public class SchedulingEngine( @@ -801,23 +859,46 @@ public class SchedulingEngine(
return offlineTail ? targetTime : _state.CurrentTime;
}
private void AddFillerPlaylist(string playlist, FillerKind fillerKind)
{
if (!_enumerators.TryGetValue(playlist, out EnumeratorDetails enumeratorDetails))
{
logger.LogWarning("Skipping invalid filler playlist {Key}", playlist);
return;
}
if (enumeratorDetails.Enumerator is PlaylistEnumerator playlistEnumerator)
{
int count = playlistEnumerator.CountForFiller;
AddCountInternal(
enumeratorDetails,
count,
fillerKind,
customTitle: null,
disableWatermarks: true,
disableFiller: true);
}
}
private bool AddCountInternal(
EnumeratorDetails enumeratorDetails,
int count,
Option<FillerKind> fillerKind,
string customTitle,
bool disableWatermarks)
bool disableWatermarks,
bool disableFiller = false)
{
var result = false;
for (var i = 0; i < count; i++)
{
// foreach (string preRollSequence in context.GetPreRollSequence())
// {
// context.PushFillerKind(FillerKind.PreRoll);
// await executeSequence(preRollSequence);
// context.PopFillerKind();
// }
if (!disableFiller)
{
foreach (string preRollPlaylist in _state.GetPreRollPlaylist())
{
AddFillerPlaylist(preRollPlaylist, FillerKind.PreRoll);
}
}
foreach (MediaItem mediaItem in enumeratorDetails.Enumerator.Current)
{
@ -978,6 +1059,10 @@ public class SchedulingEngine( @@ -978,6 +1059,10 @@ public class SchedulingEngine(
}
}
public void PreRollOn(string content) => _state.PreRollOn(content);
public void PreRollOff() => _state.PreRollOff();
public void SkipItems(string content, int count)
{
if (!_enumerators.TryGetValue(content, out EnumeratorDetails enumeratorDetails))
@ -1365,11 +1450,6 @@ public class SchedulingEngine( @@ -1365,11 +1450,6 @@ public class SchedulingEngine(
return fillerKind;
}
// foreach (FillerKind fillerKind in _state.GetFillerKind())
// {
// return fillerKind;
// }
return FillerKind.None;
}
@ -1422,7 +1502,8 @@ public class SchedulingEngine( @@ -1422,7 +1502,8 @@ public class SchedulingEngine(
public record SerializedState(
int? GuideGroup,
bool? GuideGroupLocked);
bool? GuideGroupLocked,
string PreRollPlaylist);
private class SchedulingEngineState(int guideGroup) : ISchedulingEngineState
{
@ -1430,6 +1511,8 @@ public class SchedulingEngine( @@ -1430,6 +1511,8 @@ public class SchedulingEngine(
private bool _guideGroupLocked;
private readonly Dictionary<int, string> _graphicsElements = [];
private readonly System.Collections.Generic.HashSet<int> _channelWatermarkIds = [];
private readonly Stack<FillerKind> _fillerKind = new();
private Option<string> _preRollPlaylist = Option<string>.None;
// track is_done calls when current_time has not advanced
private DateTimeOffset _lastCheckedTime;
@ -1497,6 +1580,10 @@ public class SchedulingEngine( @@ -1497,6 +1580,10 @@ public class SchedulingEngine(
public void ClearChannelWatermarkIds() => _channelWatermarkIds.Clear();
public List<int> GetChannelWatermarkIds() => _channelWatermarkIds.ToList();
public void PreRollOn(string playlist) => _preRollPlaylist = playlist;
public void PreRollOff() => _preRollPlaylist = Option<string>.None;
public Option<string> GetPreRollPlaylist() => _preRollPlaylist;
// result
public Option<DateTimeOffset> RemoveBefore { get; set; }
public bool ClearItems { get; set; }
@ -1529,15 +1616,16 @@ public class SchedulingEngine( @@ -1529,15 +1616,16 @@ public class SchedulingEngine(
public string SerializeContext()
{
// string preRollSequence = null;
// foreach (string sequence in _preRollSequence)
// {
// preRollSequence = sequence;
// }
string preRollPlaylist = null;
foreach (string playlist in _preRollPlaylist)
{
preRollPlaylist = playlist;
}
var state = new SerializedState(
_guideGroup,
_guideGroupLocked);
_guideGroupLocked,
preRollPlaylist);
return JsonConvert.SerializeObject(state, Formatting.None, JsonSettings);
}
@ -1555,9 +1643,4 @@ public class SchedulingEngine( @@ -1555,9 +1643,4 @@ public class SchedulingEngine(
}
}
}
private record EnumeratorDetails(
IMediaCollectionEnumerator Enumerator,
string HistoryKey,
PlaybackOrder PlaybackOrder);
}

11
ErsatzTV.Core/Scheduling/HistoryDetails.cs

@ -75,6 +75,17 @@ internal static class HistoryDetails @@ -75,6 +75,17 @@ internal static class HistoryDetails
return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings);
}
public static string KeyForSchedulingPlaylistContent(string key)
{
var historyKey = new Dictionary<string, object>
{
{ "Key", key },
{ "Order", nameof(PlaybackOrder.None) }
};
return JsonConvert.SerializeObject(historyKey, Formatting.None, JsonSettings);
}
public static string KeyForCollectionKey(CollectionKey collectionKey)
{
dynamic key = new

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

@ -121,7 +121,7 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor @@ -121,7 +121,7 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
itemPlaybackOrder = PlaybackOrder.Shuffle;
}
Option<MarathonContentResult> maybeResult = await helper.GetEnumerator(
Option<PlaylistContentResult> maybeResult = await helper.GetEnumerator(
guids,
marathon.Searches,
marathon.GroupBy,
@ -131,7 +131,7 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor @@ -131,7 +131,7 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor
state,
cancellationToken);
foreach (MarathonContentResult result in maybeResult)
foreach (PlaylistContentResult result in maybeResult)
{
foreach ((CollectionKey collectionKey, List<MediaItem> mediaItems) in result.Content)
{

53
ErsatzTV/Controllers/Api/ScriptedScheduleController.cs

@ -123,6 +123,29 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP @@ -123,6 +123,29 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP
return Ok();
}
[HttpPost("create_playlist", Name="CreatePlaylist")]
[Tags("Scripted Content")]
[EndpointSummary("Create a playlist")]
public async Task<IActionResult> CreatePlaylist(
[FromRoute]
Guid buildId,
[FromBody]
ContentCreatePlaylist request,
CancellationToken cancellationToken)
{
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId);
if (engine == null)
{
return NotFound($"Active build engine not found for build {buildId}.");
}
await engine.CreatePlaylist(
request.Key,
request.Items.ToDictionary(i => i.Content, i => i.Count),
cancellationToken);
return Ok();
}
[HttpPost("add_search", Name = "AddSearch")]
[Tags("Scripted Content")]
[EndpointSummary("Add a search query")]
@ -510,6 +533,36 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP @@ -510,6 +533,36 @@ public class ScriptedScheduleController(IScriptedPlayoutBuilderService scriptedP
return Ok();
}
[HttpPost("pre_roll_on", Name = "PreRollOn")]
[Tags("Scripted Control")]
[EndpointSummary("Turn on pre-roll playlist")]
public IActionResult PreRollOn([FromRoute] Guid buildId, [FromBody] ControlPreRollOn request)
{
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId);
if (engine == null)
{
return NotFound($"Active build engine not found for build {buildId}.");
}
engine.PreRollOn(request.Playlist);
return Ok();
}
[HttpPost("pre_roll_off", Name = "PreRollOff")]
[Tags("Scripted Control")]
[EndpointSummary("Turn off pre-roll playlist")]
public IActionResult PreRollOff([FromRoute] Guid buildId)
{
ISchedulingEngine engine = scriptedPlayoutBuilderService.GetEngine(buildId);
if (engine == null)
{
return NotFound($"Active build engine not found for build {buildId}.");
}
engine.PreRollOff();
return Ok();
}
[HttpPost("skip_items", Name = "SkipItems")]
[Tags("Scripted Control")]
[EndpointSummary("Skip a specific number of items")]

167
ErsatzTV/wwwroot/openapi/scripted-schedule-tagged.json

@ -247,6 +247,56 @@ @@ -247,6 +247,56 @@
}
}
},
"/api/scripted/playout/build/{buildId}/create_playlist": {
"post": {
"tags": [
"Scripted Content"
],
"summary": "Create a playlist",
"operationId": "CreatePlaylist",
"parameters": [
{
"name": "buildId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/scripted/playout/build/{buildId}/add_search": {
"post": {
"tags": [
@ -1124,6 +1174,81 @@ @@ -1124,6 +1174,81 @@
}
}
},
"/api/scripted/playout/build/{buildId}/pre_roll_on": {
"post": {
"tags": [
"Scripted Control"
],
"summary": "Turn on pre-roll playlist",
"operationId": "PreRollOn",
"parameters": [
{
"name": "buildId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/scripted/playout/build/{buildId}/pre_roll_off": {
"post": {
"tags": [
"Scripted Control"
],
"summary": "Turn off pre-roll playlist",
"operationId": "PreRollOff",
"parameters": [
{
"name": "buildId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/scripted/playout/build/{buildId}/skip_items": {
"post": {
"tags": [
@ -1404,6 +1529,24 @@ @@ -1404,6 +1529,24 @@
}
}
},
"ContentCreatePlaylist": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Unique name used to reference this content throughout the scripted schedule",
"nullable": true
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PlaylistItem"
},
"description": "List of playlist items",
"nullable": true
}
}
},
"ContentMarathon": {
"type": "object",
"properties": {
@ -1584,6 +1727,16 @@ @@ -1584,6 +1727,16 @@
}
}
},
"ControlPreRollOn": {
"type": "object",
"properties": {
"playlist": {
"type": "string",
"description": "The 'key' for the scripted playlist",
"nullable": true
}
}
},
"ControlSkipItems": {
"type": "object",
"properties": {
@ -1700,6 +1853,20 @@ @@ -1700,6 +1853,20 @@
}
}
},
"PlaylistItem": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The 'key' for the content",
"nullable": true
},
"count": {
"type": "integer",
"format": "int32"
}
}
},
"PlayoutContext": {
"type": "object",
"properties": {

167
ErsatzTV/wwwroot/openapi/scripted-schedule.json

@ -247,6 +247,56 @@ @@ -247,6 +247,56 @@
}
}
},
"/api/scripted/playout/build/{buildId}/create_playlist": {
"post": {
"tags": [
"ScriptedSchedule"
],
"summary": "Create a playlist",
"operationId": "CreatePlaylist",
"parameters": [
{
"name": "buildId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ContentCreatePlaylist"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/scripted/playout/build/{buildId}/add_search": {
"post": {
"tags": [
@ -1124,6 +1174,81 @@ @@ -1124,6 +1174,81 @@
}
}
},
"/api/scripted/playout/build/{buildId}/pre_roll_on": {
"post": {
"tags": [
"ScriptedSchedule"
],
"summary": "Turn on pre-roll playlist",
"operationId": "PreRollOn",
"parameters": [
{
"name": "buildId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"requestBody": {
"content": {
"application/json-patch+json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/ControlPreRollOn"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/scripted/playout/build/{buildId}/pre_roll_off": {
"post": {
"tags": [
"ScriptedSchedule"
],
"summary": "Turn off pre-roll playlist",
"operationId": "PreRollOff",
"parameters": [
{
"name": "buildId",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/scripted/playout/build/{buildId}/skip_items": {
"post": {
"tags": [
@ -1404,6 +1529,24 @@ @@ -1404,6 +1529,24 @@
}
}
},
"ContentCreatePlaylist": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "Unique name used to reference this content throughout the scripted schedule",
"nullable": true
},
"items": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PlaylistItem"
},
"description": "List of playlist items",
"nullable": true
}
}
},
"ContentMarathon": {
"type": "object",
"properties": {
@ -1584,6 +1727,16 @@ @@ -1584,6 +1727,16 @@
}
}
},
"ControlPreRollOn": {
"type": "object",
"properties": {
"playlist": {
"type": "string",
"description": "The 'key' for the scripted playlist",
"nullable": true
}
}
},
"ControlSkipItems": {
"type": "object",
"properties": {
@ -1700,6 +1853,20 @@ @@ -1700,6 +1853,20 @@
}
}
},
"PlaylistItem": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The 'key' for the content",
"nullable": true
},
"count": {
"type": "integer",
"format": "int32"
}
}
},
"PlayoutContext": {
"type": "object",
"properties": {

Loading…
Cancel
Save