From 2cb0d127018feb0021f92f96a744e1f1fc14a99e Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:00:55 +0000 Subject: [PATCH] load a configurable number of epg entries for text graphics (#2305) * wip * load a configurable number of epg entries for text graphics * cleanup --- CHANGELOG.md | 3 +- ErsatzTV.Core/Graphics/TextGraphicsElement.cs | 3 ++ .../Repositories/ITemplateDataRepository.cs | 7 ++- .../Metadata/EpgProgrammeTemplateData.cs | 13 +++++ ErsatzTV.Core/Metadata/EpgTemplateDataKey.cs | 7 +-- .../Repositories/TemplateDataRepository.cs | 53 +++++++++++++++---- ErsatzTV.Infrastructure/Epg/EpgReader.cs | 51 +++++++++++++++--- .../Streaming/GraphicsEngine.cs | 3 +- .../Streaming/TextElement.cs | 2 +- 9 files changed, 113 insertions(+), 29 deletions(-) create mode 100644 ErsatzTV.Core/Metadata/EpgProgrammeTemplateData.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad987e6..e1aa3c28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Displays multi-line text in a specified font, color, location, z-index - Supports constant opacity and opacity expression - Supports EPG and Media Item variable replacement - - EPG data is sourced from XMLTV + - EPG data is sourced from XMLTV for the current time + - EPG data can also load a configurable number of subsequent (up next) entries - Media Item data is sourced from the currently playing media item - Add `image` graphics element type - Supported in playback troubleshooting and YAML playouts diff --git a/ErsatzTV.Core/Graphics/TextGraphicsElement.cs b/ErsatzTV.Core/Graphics/TextGraphicsElement.cs index ba468c69..a5de5323 100644 --- a/ErsatzTV.Core/Graphics/TextGraphicsElement.cs +++ b/ErsatzTV.Core/Graphics/TextGraphicsElement.cs @@ -33,6 +33,9 @@ public class TextGraphicsElement [YamlMember(Alias = "font_color", ApplyNamingConventions = false)] public string FontColor { get; set; } + [YamlMember(Alias = "epg_entries", ApplyNamingConventions = false)] + public int EpgEntries { get; set; } + public string Text { get; set; } public static async Task> FromFile(string fileName) diff --git a/ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs index e1da3e38..05f05549 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/ITemplateDataRepository.cs @@ -6,5 +6,8 @@ public interface ITemplateDataRepository { public Task>> GetMediaItemTemplateData(MediaItem mediaItem); - public Task>> GetEpgTemplateData(string channelNumber, DateTimeOffset time); -} \ No newline at end of file + public Task>> GetEpgTemplateData( + string channelNumber, + DateTimeOffset time, + int count); +} diff --git a/ErsatzTV.Core/Metadata/EpgProgrammeTemplateData.cs b/ErsatzTV.Core/Metadata/EpgProgrammeTemplateData.cs new file mode 100644 index 00000000..e1a571ad --- /dev/null +++ b/ErsatzTV.Core/Metadata/EpgProgrammeTemplateData.cs @@ -0,0 +1,13 @@ +namespace ErsatzTV.Core.Metadata; + +public class EpgProgrammeTemplateData +{ + public DateTime Start { get; set; } + public DateTime Stop { get; set; } + public string Title { get; set; } + public string SubTitle { get; set; } + public string Description { get; set; } + public string Rating { get; set; } + public string[] Categories { get; set; } + public string Date { get; set; } +} diff --git a/ErsatzTV.Core/Metadata/EpgTemplateDataKey.cs b/ErsatzTV.Core/Metadata/EpgTemplateDataKey.cs index 6e5240eb..4c2d2b40 100644 --- a/ErsatzTV.Core/Metadata/EpgTemplateDataKey.cs +++ b/ErsatzTV.Core/Metadata/EpgTemplateDataKey.cs @@ -2,10 +2,5 @@ namespace ErsatzTV.Core.Metadata; public static class EpgTemplateDataKey { - public static readonly string Title = "Epg_Title"; - public static readonly string SubTitle = "Epg_SubTitle"; - public static readonly string Description = "Epg_Description"; - public static readonly string Rating = "Epg_Rating"; - public static readonly string Categories = "Epg_Categories"; - public static readonly string Date = "Epg_Date"; + public static readonly string Epg = "Epg"; } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs index 789f5d6c..e5adcd77 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TemplateDataRepository.cs @@ -1,3 +1,4 @@ +using System.Globalization; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Extensions; @@ -22,7 +23,10 @@ public class TemplateDataRepository(ILocalFileSystem localFileSystem, IDbContext _ => Option>.None }; - public async Task>> GetEpgTemplateData(string channelNumber, DateTimeOffset time) + public async Task>> GetEpgTemplateData( + string channelNumber, + DateTimeOffset time, + int count) { try { @@ -30,19 +34,48 @@ public class TemplateDataRepository(ILocalFileSystem localFileSystem, IDbContext if (localFileSystem.FileExists(targetFile)) { await using var stream = File.OpenRead(targetFile); - var maybeEpgProgramme = EpgReader.FindProgrammeAt(stream, time); - foreach (var epgProgramme in maybeEpgProgramme) + var xmlProgrammes = EpgReader.FindProgrammesAt(stream, time, count); + var result = new List(); + + foreach (var epgProgramme in xmlProgrammes) { - return new Dictionary + var data = new EpgProgrammeTemplateData { - [EpgTemplateDataKey.Title] = epgProgramme.Title?.Value, - [EpgTemplateDataKey.SubTitle] = epgProgramme.SubTitle?.Value, - [EpgTemplateDataKey.Description] = epgProgramme.Description?.Value, - [EpgTemplateDataKey.Rating] = epgProgramme.Rating?.Value, - [EpgTemplateDataKey.Categories] = (epgProgramme.Categories ?? []).Map(c => c.Value).ToArray(), - [EpgTemplateDataKey.Date] = epgProgramme.Date?.Value + Title = epgProgramme.Title?.Value, + SubTitle = epgProgramme.SubTitle?.Value, + Description = epgProgramme.Description?.Value, + Rating = epgProgramme.Rating?.Value, + Categories = (epgProgramme.Categories ?? []).Map(c => c.Value).ToArray(), + Date = epgProgramme.Date?.Value }; + + if (DateTimeOffset.TryParseExact( + epgProgramme.Start, + EpgReader.XmlTvDateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var start)) + { + data.Start = start.LocalDateTime; + } + + if (DateTimeOffset.TryParseExact( + epgProgramme.Stop, + EpgReader.XmlTvDateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var stop)) + { + data.Stop = stop.LocalDateTime; + } + + result.Add(data); } + + return new Dictionary + { + [EpgTemplateDataKey.Epg] = result + }; } } catch (Exception e) diff --git a/ErsatzTV.Infrastructure/Epg/EpgReader.cs b/ErsatzTV.Infrastructure/Epg/EpgReader.cs index 4a9484a6..9bd93018 100644 --- a/ErsatzTV.Infrastructure/Epg/EpgReader.cs +++ b/ErsatzTV.Infrastructure/Epg/EpgReader.cs @@ -7,12 +7,13 @@ namespace ErsatzTV.Infrastructure.Epg; public static class EpgReader { - private const string XmlTvDateFormat = "yyyyMMddHHmmss zzz"; + public const string XmlTvDateFormat = "yyyyMMddHHmmss zzz"; - public static Option FindProgrammeAt(Stream xmlStream, DateTimeOffset targetTime) + public static List FindProgrammesAt(Stream xmlStream, DateTimeOffset targetTime, int count) { - var serializer = new XmlSerializer(typeof(EpgProgramme)); + var result = new List(); + var serializer = new XmlSerializer(typeof(EpgProgramme)); var settings = new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment @@ -20,13 +21,29 @@ public static class EpgReader using var reader = XmlReader.Create(xmlStream, settings); - while (reader.Read()) + var foundCurrent = false; + + while (reader.Read() && count > 0) { if (reader.NodeType != XmlNodeType.Element || reader.Name != "programme") { continue; } + if (foundCurrent) + { + using var subtreeReader = reader.ReadSubtree(); + var maybeSubtreeProgramme = Optional(serializer.Deserialize(subtreeReader) as EpgProgramme); + result.AddRange(maybeSubtreeProgramme); + if (maybeSubtreeProgramme.IsNone) + { + return result; + } + + count--; + continue; + } + string startStr = reader.GetAttribute("start"); string stopStr = reader.GetAttribute("stop"); @@ -35,17 +52,35 @@ public static class EpgReader continue; } - if (DateTimeOffset.TryParseExact(startStr, XmlTvDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var start) && - DateTimeOffset.TryParseExact(stopStr, XmlTvDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out var stop)) + if (DateTimeOffset.TryParseExact( + startStr, + XmlTvDateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var start) && + DateTimeOffset.TryParseExact( + stopStr, + XmlTvDateFormat, + CultureInfo.InvariantCulture, + DateTimeStyles.None, + out var stop)) { if (start <= targetTime && targetTime < stop) { using var subtreeReader = reader.ReadSubtree(); - return Optional(serializer.Deserialize(subtreeReader) as EpgProgramme); + var maybeCurrentProgramme = Optional(serializer.Deserialize(subtreeReader) as EpgProgramme); + result.AddRange(maybeCurrentProgramme); + if (maybeCurrentProgramme.IsNone) + { + return result; + } + + foundCurrent = true; + count--; } } } - return Option.None; + return result; } } diff --git a/ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs b/ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs index 82e5360a..ce0a345a 100644 --- a/ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs +++ b/ErsatzTV.Infrastructure/Streaming/GraphicsEngine.cs @@ -37,9 +37,10 @@ public class GraphicsEngine(ITemplateDataRepository templateDataRepository, ILog } // epg variables + int maxEpg = context.Elements.OfType().Max(c => c.TextElement.EpgEntries); var startTime = context.ContentStartTime + context.Seek; var maybeEpgData = - await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime); + await templateDataRepository.GetEpgTemplateData(context.ChannelNumber, startTime, maxEpg); foreach (var templateData in maybeEpgData) { foreach (var variable in templateData) diff --git a/ErsatzTV.Infrastructure/Streaming/TextElement.cs b/ErsatzTV.Infrastructure/Streaming/TextElement.cs index 8b6a1b2f..81c2cfd0 100644 --- a/ErsatzTV.Infrastructure/Streaming/TextElement.cs +++ b/ErsatzTV.Infrastructure/Streaming/TextElement.cs @@ -45,7 +45,7 @@ public class TextElement(TextGraphicsElement textElement, Dictionary member.Name); var font = GraphicsEngineFonts.GetFont(textElement.FontFamily, textElement.FontSize ?? 48, FontStyle.Regular);