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);