diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutParsedSchedule.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutParsedSchedule.cs new file mode 100644 index 000000000..6871c85f4 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutParsedSchedule.cs @@ -0,0 +1,76 @@ +using System.Globalization; +using ErsatzTV.Core.Domain.Scheduling; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; + +public class YamlPlayoutParsedSchedule : IAlternateScheduleItem +{ + public YamlPlayoutParsedSchedule(YamlPlayoutSchedule schedule, int index) + { + Schedule = schedule; + Index = index; + + // TODO: these should be sourced from the schedule + DaysOfWeek = AlternateScheduleSelector.AllDaysOfWeek(); + DaysOfMonth = AlternateScheduleSelector.AllDaysOfMonth(); + MonthsOfYear = AlternateScheduleSelector.AllMonthsOfYear(); + + Option<(int month, int day, int? year)> maybeStart = ParseDate(schedule.StartDate); + foreach ((int month, int day, int? year) result in maybeStart) + { + (StartMonth, StartDay, StartYear) = result; + } + + Option<(int month, int day, int? year)> maybeEnd = ParseDate(schedule.EndDate); + foreach ((int month, int day, int? year) result in maybeEnd) + { + (EndMonth, EndDay, EndYear) = result; + } + + if (maybeStart.IsSome && maybeEnd.IsSome) + { + LimitToDateRange = true; + } + } + + public YamlPlayoutSchedule Schedule { get; } + public int Index { get; } + public ICollection DaysOfWeek { get; } + public ICollection DaysOfMonth { get; } + public ICollection MonthsOfYear { get; } + public bool LimitToDateRange { get; } + public int StartMonth { get; } + public int StartDay { get; } + public int? StartYear { get; } + public int EndMonth { get; } + public int EndDay { get; } + public int? EndYear { get; } + + /// + /// Parses a date string in MM-DD or YYYY-MM-DD format. + /// Returns (month, day, year) where year is null for MM-DD format. + /// + private static Option<(int month, int day, int? year)> ParseDate(string dateStr) + { + if (string.IsNullOrEmpty(dateStr)) + { + return None; + } + + 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."); + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSchedule.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSchedule.cs index 42f06c5bb..33ac51f6c 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSchedule.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSchedule.cs @@ -12,11 +12,6 @@ public class YamlPlayoutSchedule [YamlMember(Alias = "end_date", ApplyNamingConventions = false)] public string EndDate { get; set; } - /// - /// Priority for schedule matching. Higher values are checked first. Default is 0. - /// - public int Priority { get; set; } - public List Reset { get; set; } = []; public List Playout { get; set; } = []; diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs index 58e5fe9e9..ee26a7acf 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs @@ -203,11 +203,18 @@ public class SequentialPlayoutBuilder( } // Determine which schedule to use based on the start date - YamlPlayoutSchedule activeSchedule = GetActiveSchedule(playoutDefinition, start); - List resetInstructions; - List playoutInstructions; + Option maybeActiveSchedule = GetActiveSchedule(playoutDefinition, start); + List resetInstructions = []; + List playoutInstructions = []; - if (activeSchedule != null) + if (maybeActiveSchedule.IsNone) + { + logger.LogDebug("Using default playout for date {Date}", start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + resetInstructions = playoutDefinition.Reset; + playoutInstructions = playoutDefinition.Playout; + } + + foreach (YamlPlayoutSchedule activeSchedule in maybeActiveSchedule) { logger.LogInformation( "Using scheduled playout '{Name}' for date {Date}", @@ -217,12 +224,6 @@ public class SequentialPlayoutBuilder( resetInstructions = activeSchedule.Reset.Count > 0 ? activeSchedule.Reset : playoutDefinition.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) { @@ -300,15 +301,15 @@ public class SequentialPlayoutBuilder( } // handle all playout instructions - YamlPlayoutSchedule currentSchedule = activeSchedule; + Option currentSchedule = maybeActiveSchedule; while (context.CurrentTime < finish) { // Check if we've crossed into a different schedule - YamlPlayoutSchedule newSchedule = GetActiveSchedule(playoutDefinition, context.CurrentTime); - if (!ReferenceEquals(currentSchedule, newSchedule)) + Option maybeNewSchedule = GetActiveSchedule(playoutDefinition, context.CurrentTime); + if (currentSchedule != maybeNewSchedule) { - string oldName = currentSchedule != null ? (string.IsNullOrWhiteSpace(currentSchedule.Name) ? "Unnamed" : currentSchedule.Name) : "Default"; - string newName = newSchedule != null ? (string.IsNullOrWhiteSpace(newSchedule.Name) ? "Unnamed" : newSchedule.Name) : "Default"; + string oldName = currentSchedule.Match(cs => string.IsNullOrWhiteSpace(cs.Name) ? "Unnamed" : cs.Name, () => "Default"); + string newName = maybeNewSchedule.Match(ns => string.IsNullOrWhiteSpace(ns.Name) ? "Unnamed" : ns.Name, () => "Default"); logger.LogInformation( "Schedule changed from '{OldSchedule}' to '{NewSchedule}' at {Time}", oldName, @@ -316,18 +317,14 @@ public class SequentialPlayoutBuilder( context.CurrentTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)); // Switch to the new schedule - currentSchedule = newSchedule; + currentSchedule = maybeActiveSchedule; // Get the new schedule's instructions - List newPlayoutInstructions; - if (newSchedule != null) + List newPlayoutInstructions = playoutDefinition.Playout; + foreach (var newSchedule in maybeNewSchedule) { newPlayoutInstructions = newSchedule.Playout; } - else - { - newPlayoutInstructions = playoutDefinition.Playout; - } if (newPlayoutInstructions.Count == 0) { @@ -341,7 +338,7 @@ public class SequentialPlayoutBuilder( Import = playoutDefinition.Import, Content = playoutDefinition.Content, Sequence = playoutDefinition.Sequence, - Reset = newSchedule != null && newSchedule.Reset.Count > 0 ? newSchedule.Reset : playoutDefinition.Reset, + Reset = maybeNewSchedule.Match(ns => ns.Reset.Count > 0 ? ns.Reset : playoutDefinition.Reset, () => playoutDefinition.Reset), Playout = newPlayoutInstructions }; @@ -742,103 +739,17 @@ public class SequentialPlayoutBuilder( } /// - /// Finds the active schedule for the given date, or returns null if no schedule matches. - /// Schedules are checked in priority order (highest first), then by definition order. + /// Finds the active schedule for the given date, or returns None if no schedule matches. + /// Schedules are checked by definition order. /// - private static YamlPlayoutSchedule GetActiveSchedule(YamlPlayoutDefinition definition, DateTimeOffset date) + private static Option GetActiveSchedule(YamlPlayoutDefinition definition, DateTimeOffset date) { if (definition.Schedules.Count == 0) { - return null; - } - - // Sort by priority (descending), then by original order - var sortedSchedules = definition.Schedules - .Select((schedule, index) => (schedule, index)) - .OrderByDescending(x => x.schedule.Priority) - .ThenBy(x => x.index); - - foreach ((YamlPlayoutSchedule schedule, int _) in sortedSchedules) - { - if (IsDateInSchedule(schedule, date)) - { - return schedule; - } - } - - return null; - } - - /// - /// Determines if a date falls within a scheduled playout's date range. - /// - 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; - - // Handle year-specific dates (YYYY-MM-DD format) - if (startYear.HasValue && endYear.HasValue) - { - // Dates with years are always non-recurring (one-time events) - 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 - always 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; - } - - /// - /// Parses a date string in MM-DD or YYYY-MM-DD format. - /// Returns (month, day, year) where year is null for MM-DD format. - /// - 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)); + return Option.None; } - throw new FormatException($"Invalid date format: {dateStr}. Expected MM-DD or YYYY-MM-DD."); + var parsedSchedules = definition.Schedules.Select((s, i) => new YamlPlayoutParsedSchedule(s, i)).ToList(); + return AlternateScheduleSelector.GetScheduleForDate(parsedSchedules, date).Map(s => s.Schedule); } }