add new scheduling engine, basic scripted schedule system (#2337)

* start to add content to scheduling engine

* add first content instruction

* add search content

* allow scripted schedule creation

* don't use scheduling engine in sequential playout builder, yet
This commit is contained in:
Jason Dove
2025-08-24 03:11:58 +00:00
committed by GitHub
parent 53f281ce32
commit bbddd50f00
11 changed files with 902 additions and 34 deletions
@@ -1,22 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling.Engine;
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
public class ContentModule
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")]
public class ContentModule(ISchedulingEngine schedulingEngine)
{
private Dictionary<string, IMediaCollectionEnumerator> _contentEnumerators = [];
public bool add_collection(string key, string name, string order)
public bool add_search(string key, string query, string order)
{
if (_contentEnumerators.ContainsKey(key))
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder))
{
return false;
}
Console.WriteLine($"Adding collection '{name}' with key '{key}' and order '{order}'");
_contentEnumerators.Clear();
schedulingEngine.AddSearch(key, query, playbackOrder).GetAwaiter().GetResult();
return true;
}
public bool add_collection(string key, string collection, string order)
{
if (!Enum.TryParse(order, ignoreCase: true, out PlaybackOrder playbackOrder))
{
return false;
}
schedulingEngine.AddCollection(key, collection, playbackOrder).GetAwaiter().GetResult();
return true;
}
@@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Scheduling.Engine;
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
public class PlayoutModule(ISchedulingEngine schedulingEngine)
{
// content instructions
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))
{
maybeFillerKind = fillerKind;
}
schedulingEngine.AddCount(content, count, maybeFillerKind, custom_title, disable_watermarks);
}
// control instructions
public void wait_until(string when, bool tomorrow = false, bool rewind_on_reset = false)
{
if (TimeOnly.TryParse(when, out TimeOnly waitUntil))
{
schedulingEngine.WaitUntil(waitUntil, tomorrow, rewind_on_reset);
}
}
}
@@ -1,6 +1,9 @@
using System.Diagnostics.CodeAnalysis;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Scheduling.Engine;
using ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
using IronPython.Hosting;
using IronPython.Runtime;
@@ -9,8 +12,8 @@ using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling;
public class ScriptedPlayoutBuilder(
//IConfigElementRepository configElementRepository,
//IMediaCollectionRepository mediaCollectionRepository,
IConfigElementRepository configElementRepository,
ISchedulingEngine schedulingEngine,
ILocalFileSystem localFileSystem,
ILogger<ScriptedPlayoutBuilder> logger)
: IScriptedPlayoutBuilder
@@ -22,27 +25,27 @@ public class ScriptedPlayoutBuilder(
PlayoutBuildMode mode,
CancellationToken cancellationToken)
{
await Task.Delay(10, cancellationToken);
var result = PlayoutBuildResult.Empty;
if (!localFileSystem.FileExists(playout.ScheduleFile))
{
logger.LogError("Cannot build scripted playout; schedule file {File} does not exist", playout.ScheduleFile);
return result;
}
logger.LogInformation("Building scripted playout...");
//int daysToBuild = await GetDaysToBuild();
//DateTimeOffset finish = start.AddDays(daysToBuild);
//var enumeratorCache = new EnumeratorCache(mediaCollectionRepository, logger);
// apply all history???
try
{
if (!localFileSystem.FileExists(playout.ScheduleFile))
{
logger.LogError("Cannot build scripted playout; schedule file {File} does not exist", playout.ScheduleFile);
return result;
}
logger.LogInformation("Building scripted playout...");
int daysToBuild = await GetDaysToBuild();
DateTimeOffset finish = start.AddDays(daysToBuild);
schedulingEngine.WithPlayoutId(playout.Id);
schedulingEngine.WithMode(mode);
schedulingEngine.WithSeed(playout.Seed);
schedulingEngine.BuildBetween(start, finish);
schedulingEngine.WithReferenceData(referenceData);
var engine = Python.CreateEngine();
var scope = engine.CreateScope();
@@ -55,10 +58,48 @@ public class ScriptedPlayoutBuilder(
dynamic ersatztv = engine.Operations.Invoke(moduleType, "ersatztv");
modules["ersatztv"] = ersatztv;
var contentModule = new ContentModule();
var contentModule = new ContentModule(schedulingEngine);
engine.Operations.SetMember(ersatztv, "content", contentModule);
var playoutModule = new PlayoutModule(schedulingEngine);
engine.Operations.SetMember(ersatztv, "playout", playoutModule);
engine.ExecuteFile(playout.ScheduleFile, scope);
// define_content is required
if (!scope.TryGetVariable("define_content", out PythonFunction defineContentFunc))
{
logger.LogError("Script must contain a 'define_content' function");
return result;
}
// reset_playout is NOT required
scope.TryGetVariable("reset_playout", out PythonFunction resetPlayoutFunc);
// build_playout is required
if (!scope.TryGetVariable("build_playout", out PythonFunction buildPlayoutFunc))
{
logger.LogError("Script must contain a 'build_playout' function");
return result;
}
schedulingEngine.RestoreOrReset(Optional(playout.Anchor));
// define content first
engine.Operations.Invoke(defineContentFunc);
// reset if applicable
if (mode is PlayoutBuildMode.Reset && resetPlayoutFunc != null)
{
engine.Operations.Invoke(resetPlayoutFunc, new PythonPlayoutContext(schedulingEngine.GetState()));
}
// build playout
engine.Operations.Invoke(buildPlayoutFunc, new PythonPlayoutContext(schedulingEngine.GetState()));
playout.Anchor = schedulingEngine.GetAnchor();
result = MergeResult(result, schedulingEngine.GetState());
}
catch (Exception ex)
{
@@ -69,8 +110,28 @@ public class ScriptedPlayoutBuilder(
return result;
}
// private async Task<int> GetDaysToBuild() =>
// await configElementRepository
// .GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
// .IfNoneAsync(2);
private async Task<int> GetDaysToBuild() =>
await configElementRepository
.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.IfNoneAsync(2);
private static PlayoutBuildResult MergeResult(PlayoutBuildResult result, ISchedulingEngineState state) =>
result with
{
ClearItems = state.ClearItems,
RemoveBefore = state.RemoveBefore,
AddedItems = state.AddedItems,
//ItemsToRemove = state.ItemsToRemove,
AddedHistory = state.AddedHistory,
HistoryToRemove = state.HistoryToRemove
};
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
public class PythonPlayoutContext(ISchedulingEngineState state)
{
public DateTimeOffset current_time => state.CurrentTime;
public DateTimeOffset finish => state.Finish;
public bool is_done() => current_time >= finish;
}
}