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,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;
}
}