Browse Source

You can now have `schedules` in a yaml playback which define playouts during a period of time

pull/2682/head
Carson Kompon 8 months ago committed by Jason Dove
parent
commit
590da4ab24
No known key found for this signature in database
  1. 11
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDefinition.cs
  2. 28
      ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSchedule.cs
  3. 158
      ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs
  4. 87
      ErsatzTV/Resources/sequential-schedule.schema.json

11
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDefinition.cs

@ -8,7 +8,18 @@ public class YamlPlayoutDefinition @@ -8,7 +8,18 @@ public class YamlPlayoutDefinition
public List<YamlPlayoutSequenceItem> Sequence { get; set; } = [];
/// <summary>
/// Scheduled playouts that apply to specific date ranges
/// </summary>
public List<YamlPlayoutSchedule> Schedules { get; set; } = [];
/// <summary>
/// Default reset instructions (used when no schedule matches)
/// </summary>
public List<YamlPlayoutInstruction> Reset { get; set; } = [];
/// <summary>
/// Default playout instructions (used when no schedule matches)
/// </summary>
public List<YamlPlayoutInstruction> Playout { get; set; } = [];
}

28
ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSchedule.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using YamlDotNet.Serialization;
namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models;
public class YamlPlayoutSchedule
{
public string Name { get; set; }
[YamlMember(Alias = "start_date", ApplyNamingConventions = false)]
public string StartDate { get; set; }
[YamlMember(Alias = "end_date", ApplyNamingConventions = false)]
public string EndDate { get; set; }
/// <summary>
/// Specific year(s) this schedule applies to. If empty, applies to all years.
/// </summary>
public List<int> Years { get; set; } = [];
/// <summary>
/// Whether this schedule repeats annually (default: true when no years are specified)
/// </summary>
public bool Recurring { get; set; } = true;
public List<YamlPlayoutInstruction> Reset { get; set; } = [];
public List<YamlPlayoutInstruction> Playout { get; set; } = [];
}

158
ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Globalization;
using System.IO.Abstractions;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
@ -201,10 +202,37 @@ public class SequentialPlayoutBuilder( @@ -201,10 +202,37 @@ public class SequentialPlayoutBuilder(
cancellationToken);
}
// Determine which schedule to use based on the start date
YamlPlayoutSchedule activeSchedule = GetActiveSchedule(playoutDefinition, start);
List<YamlPlayoutInstruction> resetInstructions;
List<YamlPlayoutInstruction> playoutInstructions;
if (activeSchedule != null)
{
logger.LogInformation(
"Using scheduled playout '{Name}' for date {Date}",
string.IsNullOrWhiteSpace(activeSchedule.Name) ? "Unnamed" : activeSchedule.Name,
start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
resetInstructions = activeSchedule.Reset;
playoutInstructions = activeSchedule.Playout;
}
else
{
logger.LogDebug("Using default playout for date {Date}", start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
resetInstructions = playoutDefinition.Reset;
playoutInstructions = playoutDefinition.Playout;
}
if (playoutInstructions.Count == 0)
{
logger.LogWarning("No playout instructions found for date {Date}", start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
return BaseError.New($"No playout instructions found for date {start:yyyy-MM-dd}");
}
if (mode is PlayoutBuildMode.Reset)
{
// handle all on-reset instructions
foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset)
foreach (YamlPlayoutInstruction instruction in resetInstructions)
{
Option<IYamlPlayoutHandler> maybeHandler = GetHandlerForInstruction(
handlers,
@ -233,6 +261,23 @@ public class SequentialPlayoutBuilder( @@ -233,6 +261,23 @@ public class SequentialPlayoutBuilder(
}
}
// Create a temporary modified definition with the selected instructions
var effectiveDefinition = new YamlPlayoutDefinition
{
Import = playoutDefinition.Import,
Content = playoutDefinition.Content,
Sequence = playoutDefinition.Sequence,
Reset = resetInstructions,
Playout = playoutInstructions
};
// Update context to use the effective definition
context = new YamlPlayoutContext(playout, effectiveDefinition, 1)
{
CurrentTime = context.CurrentTime,
InstructionIndex = context.InstructionIndex
};
if (DetectCycle(context.Definition))
{
logger.LogError("YAML sequence contains a cycle; unable to build playout");
@ -256,13 +301,13 @@ public class SequentialPlayoutBuilder( @@ -256,13 +301,13 @@ public class SequentialPlayoutBuilder(
// handle all playout instructions
while (context.CurrentTime < finish)
{
if (context.InstructionIndex >= playoutDefinition.Playout.Count)
if (context.InstructionIndex >= effectiveDefinition.Playout.Count)
{
logger.LogInformation("Reached the end of the YAML playout definition; stopping");
break;
}
YamlPlayoutInstruction instruction = playoutDefinition.Playout[context.InstructionIndex];
YamlPlayoutInstruction instruction = effectiveDefinition.Playout[context.InstructionIndex];
//logger.LogDebug("Current playout instruction: {Instruction}", instruction.GetType().Name);
Option<IYamlPlayoutHandler> maybeHandler = GetHandlerForInstruction(handlers, enumeratorCache, instruction);
@ -582,4 +627,111 @@ public class SequentialPlayoutBuilder( @@ -582,4 +627,111 @@ public class SequentialPlayoutBuilder(
return result;
}
/// <summary>
/// Finds the active schedule for the given date, or returns null if no schedule matches.
/// </summary>
private static YamlPlayoutSchedule GetActiveSchedule(YamlPlayoutDefinition definition, DateTimeOffset date)
{
if (definition.Schedules.Count == 0)
{
return null;
}
foreach (YamlPlayoutSchedule schedule in definition.Schedules)
{
if (IsDateInSchedule(schedule, date))
{
return schedule;
}
}
return null;
}
/// <summary>
/// Determines if a date falls within a scheduled playout's date range.
/// </summary>
private static bool IsDateInSchedule(YamlPlayoutSchedule schedule, DateTimeOffset date)
{
if (string.IsNullOrWhiteSpace(schedule.StartDate) || string.IsNullOrWhiteSpace(schedule.EndDate))
{
return false;
}
(int startMonth, int startDay, int? startYear) = ParseDate(schedule.StartDate);
(int endMonth, int endDay, int? endYear) = ParseDate(schedule.EndDate);
int currentYear = date.Year;
int currentMonth = date.Month;
int currentDay = date.Day;
// Check if years are specified
if (schedule.Years.Count > 0)
{
if (!schedule.Years.Contains(currentYear))
{
return false;
}
}
else if (!schedule.Recurring)
{
// If not recurring and no years specified, the schedule doesn't apply
return false;
}
// Handle year-specific dates
if (startYear.HasValue && endYear.HasValue)
{
var startDate = new DateTime(startYear.Value, startMonth, startDay);
var endDate = new DateTime(endYear.Value, endMonth, endDay);
var currentDate = new DateTime(currentYear, currentMonth, currentDay);
return currentDate >= startDate && currentDate <= endDate;
}
if (startYear.HasValue || endYear.HasValue)
{
// Mixed format - not supported
return false;
}
// Both dates are MM-DD format (recurring)
var currentDayOfYear = new DateTime(currentYear, currentMonth, currentDay).DayOfYear;
var startDayOfYear = new DateTime(currentYear, startMonth, startDay).DayOfYear;
var endDayOfYear = new DateTime(currentYear, endMonth, endDay).DayOfYear;
// Handle date ranges that span across the new year
if (startDayOfYear <= endDayOfYear)
{
// Normal range (e.g., Jan 1 - March 31)
return currentDayOfYear >= startDayOfYear && currentDayOfYear <= endDayOfYear;
}
// Range wraps around the year (e.g., Dec 1 - Jan 31)
return currentDayOfYear >= startDayOfYear || currentDayOfYear <= endDayOfYear;
}
/// <summary>
/// Parses a date string in MM-DD or YYYY-MM-DD format.
/// Returns (month, day, year) where year is null for MM-DD format.
/// </summary>
private static (int month, int day, int? year) ParseDate(string dateStr)
{
string[] parts = dateStr.Split('-');
if (parts.Length == 2)
{
// MM-DD format
return (int.Parse(parts[0], CultureInfo.InvariantCulture), int.Parse(parts[1], CultureInfo.InvariantCulture), null);
}
if (parts.Length == 3)
{
// YYYY-MM-DD format
return (int.Parse(parts[1], CultureInfo.InvariantCulture), int.Parse(parts[2], CultureInfo.InvariantCulture), int.Parse(parts[0], CultureInfo.InvariantCulture));
}
throw new FormatException($"Invalid date format: {dateStr}. Expected MM-DD or YYYY-MM-DD.");
}
}

87
ErsatzTV/Resources/sequential-schedule.schema.json

@ -66,8 +66,81 @@ @@ -66,8 +66,81 @@
"additionalProperties": false
}
},
"schedules": {
"description": "Scheduled playouts for specific date ranges",
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Optional name for this schedule (for debugging/logging)"
},
"start_date": {
"type": "string",
"description": "Start date in MM-DD (e.g., '01-01') or YYYY-MM-DD format (e.g., '2024-12-25')",
"pattern": "^((\\d{4}-)?(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]))$"
},
"end_date": {
"type": "string",
"description": "End date in MM-DD (e.g., '01-31') or YYYY-MM-DD format (e.g., '2024-12-25')",
"pattern": "^((\\d{4}-)?(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]))$"
},
"years": {
"type": "array",
"items": { "type": "integer" },
"description": "Specific years this schedule applies to. If empty, applies to all years (when recurring is true)."
},
"recurring": {
"type": "boolean",
"description": "Whether this schedule repeats annually (default: true when no years are specified)"
},
"reset": {
"description": "Reset instructions for this scheduled playout",
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/$defs/control/rewindInstruction" },
{ "$ref": "#/$defs/control/skipItemsInstruction" },
{ "$ref": "#/$defs/control/skipToItemInstruction" },
{ "$ref": "#/$defs/control/waitUntilInstruction" }
]
}
},
"playout": {
"description": "Playout instructions for this scheduled playout",
"type": "array",
"items": {
"oneOf": [
{ "$ref": "#/$defs/scheduling/allInstruction" },
{ "$ref": "#/$defs/scheduling/countInstruction" },
{ "$ref": "#/$defs/scheduling/durationInstruction" },
{ "$ref": "#/$defs/scheduling/padToNextInstruction" },
{ "$ref": "#/$defs/scheduling/padUntilInstruction" },
{ "$ref": "#/$defs/scheduling/sequenceInstruction" },
{ "$ref": "#/$defs/control/epgGroupInstruction" },
{ "$ref": "#/$defs/control/graphicsOnInstruction" },
{ "$ref": "#/$defs/control/graphicsOffInstruction" },
{ "$ref": "#/$defs/control/preRollInstruction"},
{ "$ref": "#/$defs/control/postRollInstruction"},
{ "$ref": "#/$defs/control/midRollInstruction"},
{ "$ref": "#/$defs/control/repeatInstruction" },
{ "$ref": "#/$defs/control/shuffleSequenceInstruction" },
{ "$ref": "#/$defs/control/skipItemsInstruction" },
{ "$ref": "#/$defs/control/skipToItemInstruction" },
{ "$ref": "#/$defs/control/waitUntilInstruction" },
{ "$ref": "#/$defs/control/watermarkInstruction" }
]
},
"minItems": 1
}
},
"required": [ "start_date", "end_date", "playout" ],
"additionalProperties": false
}
},
"reset": {
"description": "Reset instructions",
"description": "Default reset instructions (used when no schedule matches)",
"type": "array",
"items": {
"oneOf": [
@ -79,7 +152,7 @@ @@ -79,7 +152,7 @@
}
},
"playout": {
"description": "Playout instructions",
"description": "Default playout instructions (used when no schedule matches)",
"type": "array",
"items": {
"oneOf": [
@ -102,8 +175,7 @@ @@ -102,8 +175,7 @@
{ "$ref": "#/$defs/control/waitUntilInstruction" },
{ "$ref": "#/$defs/control/watermarkInstruction" }
]
},
"minItems": 1
}
}
},
"allOf": [
@ -113,7 +185,12 @@ @@ -113,7 +185,12 @@
{ "required": [ "content" ] }
]
},
{ "required": [ "playout" ] }
{
"anyOf": [
{ "required": [ "playout" ] },
{ "required": [ "schedules" ] }
]
}
],
"additionalProperties": false,
"$defs": {

Loading…
Cancel
Save