diff --git a/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs b/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs index b6ce89c3e..e2b9e27b4 100644 --- a/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs +++ b/ErsatzTV.Core/Scheduling/Engine/ISchedulingEngine.cs @@ -46,6 +46,14 @@ public interface ISchedulingEngine bool disableWatermarks); // control instructions + void LockGuideGroup(bool advance); + void UnlockGuideGroup(); + Task GraphicsOn(List graphicsElements, Dictionary variables); + Task GraphicsOff(List graphicsElements); + Task WatermarkOn(List watermarks); + Task WatermarkOff(List watermarks); + void SkipItems(string content, int count); + void SkipToItem(string content, int season, int episode); ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset); diff --git a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs index 6f1fa4afb..77d5f892c 100644 --- a/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs +++ b/ErsatzTV.Core/Scheduling/Engine/SchedulingEngine.cs @@ -11,7 +11,11 @@ using Newtonsoft.Json; namespace ErsatzTV.Core.Scheduling.Engine; -public class SchedulingEngine(IMediaCollectionRepository mediaCollectionRepository, ILogger logger) +public class SchedulingEngine( + IMediaCollectionRepository mediaCollectionRepository, + IGraphicsElementRepository graphicsElementRepository, + IChannelRepository channelRepository, + ILogger logger) : ISchedulingEngine { private static readonly JsonSerializerSettings JsonSettings = new() @@ -19,6 +23,8 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito NullValueHandling = NullValueHandling.Ignore }; + private readonly Dictionary> _graphicsElementCache = new(); + private readonly Dictionary> _watermarkCache = new(); private readonly Dictionary _enumerators = new(); private readonly SchedulingEngineState _state = new(0); private PlayoutReferenceData _referenceData; @@ -394,26 +400,26 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito PlayoutItemGraphicsElements = [] }; - // foreach (int watermarkId in context.GetChannelWatermarkIds()) - // { - // playoutItem.PlayoutItemWatermarks.Add( - // new PlayoutItemWatermark - // { - // PlayoutItem = playoutItem, - // WatermarkId = watermarkId - // }); - // } - // - // foreach ((int graphicsElementId, string variablesJson) in context.GetGraphicsElements()) - // { - // playoutItem.PlayoutItemGraphicsElements.Add( - // new PlayoutItemGraphicsElement - // { - // PlayoutItem = playoutItem, - // GraphicsElementId = graphicsElementId, - // Variables = variablesJson - // }); - // } + foreach (int watermarkId in _state.GetChannelWatermarkIds()) + { + playoutItem.PlayoutItemWatermarks.Add( + new PlayoutItemWatermark + { + PlayoutItem = playoutItem, + WatermarkId = watermarkId + }); + } + + foreach ((int graphicsElementId, string variablesJson) in _state.GetGraphicsElements()) + { + playoutItem.PlayoutItemGraphicsElements.Add( + new PlayoutItemGraphicsElement + { + PlayoutItem = playoutItem, + GraphicsElementId = graphicsElementId, + Variables = variablesJson + }); + } //await AddItemAndMidRoll(context, playoutItem, mediaItem, executeSequence); @@ -444,6 +450,133 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito return result; } + public void LockGuideGroup(bool advance) + { + _state.LockGuideGroup(advance); + } + + public void UnlockGuideGroup() + { + _state.UnlockGuideGroup(); + } + + public async Task GraphicsOn(List graphicsElements, Dictionary variables) + { + string variablesJson = null; + if (variables.Count > 0) + { + variablesJson = JsonConvert.SerializeObject(variables, JsonSettings); + } + + foreach (string element in graphicsElements.Where(e => !string.IsNullOrWhiteSpace(e))) + { + foreach (GraphicsElement ge in await GetGraphicsElementByPath(element)) + { + _state.SetGraphicsElement(ge.Id, variablesJson); + } + } + } + + public async Task GraphicsOff(List graphicsElements) + { + if (graphicsElements.Count == 0) + { + _state.ClearGraphicsElements(); + } + else + { + foreach (string element in graphicsElements.Where(e => !string.IsNullOrWhiteSpace(e))) + { + foreach (GraphicsElement ge in await GetGraphicsElementByPath(element)) + { + _state.RemoveGraphicsElement(ge.Id); + } + } + } + } + + public async Task WatermarkOn(List watermarks) + { + foreach (string watermark in watermarks.Where(e => !string.IsNullOrWhiteSpace(e))) + { + foreach (ChannelWatermark wm in await GetChannelWatermarkByName(watermark)) + { + _state.SetChannelWatermarkId(wm.Id); + } + } + } + + public async Task WatermarkOff(List watermarks) + { + if (watermarks.Count == 0) + { + _state.ClearChannelWatermarkIds(); + } + else + { + foreach (string watermark in watermarks.Where(e => !string.IsNullOrWhiteSpace(e))) + { + foreach (ChannelWatermark wm in await GetChannelWatermarkByName(watermark)) + { + _state.RemoveChannelWatermarkId(wm.Id); + } + } + } + } + + public void SkipItems(string content, int count) + { + if (!_enumerators.TryGetValue(content, out EnumeratorDetails enumeratorDetails)) + { + logger.LogWarning("Unable to skip items for invalid content {Key}", content); + return; + } + + for (var i = 0; i < count; i++) + { + enumeratorDetails.Enumerator.MoveNext(); + } + } + + public void SkipToItem(string content, int season, int episode) + { + if (!_enumerators.TryGetValue(content, out EnumeratorDetails enumeratorDetails)) + { + logger.LogWarning("Unable to skip items for invalid content {Key}", content); + return; + } + + if (season < 0 || episode < 1) + { + logger.LogWarning("Unable to skip to invalid season/episode: {Season}/{Episode}", season, episode); + return; + } + + var done = false; + for (var index = 0; index < enumeratorDetails.Enumerator.Count; index++) + { + if (done) + { + break; + } + + foreach (MediaItem mediaItem in enumeratorDetails.Enumerator.Current) + { + if (mediaItem is Episode e) + { + if (e.Season?.SeasonNumber == season && + e.EpisodeMetadata.HeadOrNone().Map(em => em.EpisodeNumber) == episode) + { + done = true; + break; + } + } + + enumeratorDetails.Enumerator.MoveNext(); + } + } + } + public ISchedulingEngine WaitUntil(TimeOnly waitUntil, bool tomorrow, bool rewindOnReset) { var currentTime = _state.CurrentTime; @@ -763,6 +896,51 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito return FillerKind.None; } + private async Task> GetGraphicsElementByPath(string path) + { + if (_graphicsElementCache.TryGetValue(path, out Option cachedGraphicsElement)) + { + foreach (GraphicsElement graphicsElement in cachedGraphicsElement) + { + return graphicsElement; + } + } + else + { + Option maybeGraphicsElement = + await graphicsElementRepository.GetGraphicsElementByPath(path); + _graphicsElementCache.Add(path, maybeGraphicsElement); + foreach (GraphicsElement graphicsElement in maybeGraphicsElement) + { + return graphicsElement; + } + } + + return Option.None; + } + + private async Task> GetChannelWatermarkByName(string name) + { + if (_watermarkCache.TryGetValue(name, out Option cachedWatermark)) + { + foreach (ChannelWatermark channelWatermark in cachedWatermark) + { + return channelWatermark; + } + } + else + { + Option maybeWatermark = await channelRepository.GetWatermarkByName(name); + _watermarkCache.Add(name, maybeWatermark); + foreach (ChannelWatermark channelWatermark in maybeWatermark) + { + return channelWatermark; + } + } + + return Option.None; + } + public record SerializedState( int? GuideGroup, bool? GuideGroupLocked); @@ -771,6 +949,8 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito { private int _guideGroup = guideGroup; private bool _guideGroupLocked; + private readonly Dictionary _graphicsElements = []; + private readonly System.Collections.Generic.HashSet _channelWatermarkIds = []; // state public int PlayoutId { get; set; } @@ -823,6 +1003,16 @@ public class SchedulingEngine(IMediaCollectionRepository mediaCollectionReposito public void UnlockGuideGroup() => _guideGroupLocked = false; + public void SetGraphicsElement(int id, string variablesJson) => _graphicsElements.Add(id, variablesJson); + public void RemoveGraphicsElement(int id) => _graphicsElements.Remove(id); + public void ClearGraphicsElements() => _graphicsElements.Clear(); + public Dictionary GetGraphicsElements() => _graphicsElements; + + public void SetChannelWatermarkId(int id) => _channelWatermarkIds.Add(id); + public void RemoveChannelWatermarkId(int id) => _channelWatermarkIds.Remove(id); + public void ClearChannelWatermarkIds() => _channelWatermarkIds.Clear(); + public List GetChannelWatermarkIds() => _channelWatermarkIds.ToList(); + // result public Option RemoveBefore { get; set; } public bool ClearItems { get; set; } diff --git a/ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/PlayoutModule.cs b/ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/PlayoutModule.cs index 187703da3..31aa46cc7 100644 --- a/ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/PlayoutModule.cs +++ b/ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/PlayoutModule.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Scheduling.Engine; +using IronPython.Runtime; namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules; @@ -41,6 +42,103 @@ public class PlayoutModule(ISchedulingEngine schedulingEngine) // control instructions + public void start_epg_group(bool advance = true) + { + schedulingEngine.LockGuideGroup(advance); + } + + public void stop_epg_group() + { + schedulingEngine.UnlockGuideGroup(); + } + + public void graphics_on(string graphics, PythonDictionary variables = null) + { + var maybeVariables = new Dictionary(); + if (variables != null) + { + maybeVariables = variables.ToDictionary(v => v.Key.ToString(), v => v.Value.ToString()); + } + + schedulingEngine + .GraphicsOn([graphics], maybeVariables) + .GetAwaiter() + .GetResult(); + } + + public void graphics_on(PythonList graphics, PythonDictionary variables = null) + { + var maybeVariables = new Dictionary(); + if (variables != null) + { + maybeVariables = variables.ToDictionary(v => v.Key.ToString(), v => v.Value.ToString()); + } + + schedulingEngine + .GraphicsOn( + graphics.Select(g => g.ToString()).ToList(), + maybeVariables) + .GetAwaiter() + .GetResult(); + } + + public void graphics_off(string graphics = null) + { + if (string.IsNullOrWhiteSpace(graphics)) + { + schedulingEngine.GraphicsOff([]).GetAwaiter().GetResult(); + } + else + { + schedulingEngine.GraphicsOff([graphics]).GetAwaiter().GetResult(); + } + } + + public void graphics_off(PythonList graphics) + { + schedulingEngine.GraphicsOff(graphics.Select(g => g.ToString()).ToList()).GetAwaiter().GetResult(); + } + + public void watermark_on(string watermark) + { + schedulingEngine.WatermarkOn([watermark]).GetAwaiter().GetResult(); + } + + public void watermark_on(PythonList watermark) + { + schedulingEngine + .WatermarkOn(watermark.Select(g => g.ToString()).ToList()) + .GetAwaiter() + .GetResult(); + } + + public void watermark_off(string watermark = null) + { + if (string.IsNullOrWhiteSpace(watermark)) + { + schedulingEngine.WatermarkOff([]).GetAwaiter().GetResult(); + } + else + { + schedulingEngine.WatermarkOff([watermark]).GetAwaiter().GetResult(); + } + } + + public void watermark_off(PythonList watermark) + { + schedulingEngine.WatermarkOff(watermark.Select(g => g.ToString()).ToList()).GetAwaiter().GetResult(); + } + + public void skip_items(string content, int count) + { + schedulingEngine.SkipItems(content, count); + } + + public void skip_to_item(string content, int season, int episode) + { + schedulingEngine.SkipToItem(content, season, episode); + } + public void wait_until(string when, bool tomorrow = false, bool rewind_on_reset = false) { if (TimeOnly.TryParse(when, out TimeOnly waitUntil)) diff --git a/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs index ff7e7babc..735fb1217 100644 --- a/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs @@ -50,6 +50,14 @@ public class ScriptedPlayoutBuilder( var engine = Python.CreateEngine(); var scope = engine.CreateScope(); + string scriptPath = Path.GetDirectoryName(playout.ScheduleFile); + if (!string.IsNullOrWhiteSpace(scriptPath) && localFileSystem.FolderExists(scriptPath)) + { + ICollection paths = engine.GetSearchPaths(); + paths.Add(scriptPath); + engine.SetSearchPaths(paths); + } + var sys = engine.GetSysModule(); var modules = (PythonDictionary)sys.GetVariable("modules");