using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using Microsoft.Extensions.Logging; using TimeSpanParserUtil; namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlPlayoutContentHandler(enumeratorCache) { public override async Task Handle( YamlPlayoutContext context, YamlPlayoutInstruction instruction, PlayoutBuildMode mode, Func executeSequence, ILogger logger, CancellationToken cancellationToken) { if (instruction is not YamlPlayoutDurationInstruction duration) { return false; } // TODO: move to up-front validation somewhere if (!TimeSpanParser.TryParse(duration.Duration, out TimeSpan timeSpan)) { return false; } if (duration.StopBeforeEnd == false && duration.OfflineTail) { logger.LogError("offline_tail must be false when stop_before_end is false"); return false; } DateTimeOffset targetTime = context.CurrentTime.Add(timeSpan); Option maybeEnumerator = await GetContentEnumerator( context, duration.Content, logger, cancellationToken); Option fallbackEnumerator = await GetContentEnumerator( context, duration.Fallback, logger, cancellationToken); foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) { context.CurrentTime = await Schedule( context, instruction.Content, duration.Fallback, targetTime, duration.StopBeforeEnd, duration.DiscardAttempts, duration.Trim, duration.OfflineTail, GetFillerKind(duration, context), duration.CustomTitle, duration.DisableWatermarks, enumerator, fallbackEnumerator, executeSequence, logger); return true; } return false; } protected static async Task Schedule( YamlPlayoutContext context, string contentKey, string fallbackContentKey, DateTimeOffset targetTime, bool stopBeforeEnd, int discardAttempts, bool trim, bool offlineTail, FillerKind fillerKind, string customTitle, bool disableWatermarks, IMediaCollectionEnumerator enumerator, Option fallbackEnumerator, Func executeSequence, ILogger logger) { var done = false; TimeSpan remainingToFill = targetTime - context.CurrentTime; while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero) { foreach (string preRollSequence in context.GetPreRollSequence()) { context.PushFillerKind(FillerKind.PreRoll); await executeSequence(preRollSequence); context.PopFillerKind(); remainingToFill = targetTime - context.CurrentTime; if (remainingToFill <= TimeSpan.Zero) { break; } } foreach (MediaItem mediaItem in enumerator.Current) { TimeSpan itemDuration = DurationForMediaItem(mediaItem); var playoutItem = new PlayoutItem { MediaItemId = mediaItem.Id, Start = context.CurrentTime.UtcDateTime, Finish = context.CurrentTime.UtcDateTime + itemDuration, InPoint = TimeSpan.Zero, OutPoint = itemDuration, GuideGroup = context.PeekNextGuideGroup(), FillerKind = fillerKind, CustomTitle = string.IsNullOrWhiteSpace(customTitle) ? null : customTitle, DisableWatermarks = disableWatermarks }; foreach (int watermarkId in context.GetChannelWatermarkId()) { playoutItem.WatermarkId = watermarkId; } if (remainingToFill - itemDuration >= TimeSpan.Zero || !stopBeforeEnd) { context.Playout.Items.Add(playoutItem); context.AdvanceGuideGroup(); // create history record List maybeHistory = GetHistoryForItem( context, contentKey, enumerator, playoutItem, mediaItem, logger); foreach (PlayoutHistory history in maybeHistory) { context.Playout.PlayoutHistory.Add(history); } remainingToFill -= itemDuration; context.CurrentTime += itemDuration; enumerator.MoveNext(); } else if (discardAttempts > 0) { // item won't fit; try the next one discardAttempts--; enumerator.MoveNext(); } else if (trim) { // trim item to exactly fit playoutItem.Finish = targetTime.UtcDateTime; playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start; context.Playout.Items.Add(playoutItem); context.AdvanceGuideGroup(); // create history record List maybeHistory = GetHistoryForItem( context, contentKey, enumerator, playoutItem, mediaItem, logger); foreach (PlayoutHistory history in maybeHistory) { context.Playout.PlayoutHistory.Add(history); } remainingToFill = TimeSpan.Zero; context.CurrentTime = targetTime; enumerator.MoveNext(); } else if (fallbackEnumerator.IsSome) { foreach (IMediaCollectionEnumerator fallback in fallbackEnumerator) { remainingToFill = TimeSpan.Zero; context.CurrentTime = targetTime; done = true; // replace with fallback content foreach (MediaItem fallbackItem in fallback.Current) { playoutItem.MediaItemId = fallbackItem.Id; playoutItem.Finish = targetTime.UtcDateTime; playoutItem.FillerKind = FillerKind.Fallback; context.Playout.Items.Add(playoutItem); // create history record List maybeHistory = GetHistoryForItem( context, fallbackContentKey, fallback, playoutItem, mediaItem, logger); foreach (PlayoutHistory history in maybeHistory) { context.Playout.PlayoutHistory.Add(history); } fallback.MoveNext(); } } } else { // item won't fit; we're done done = true; } } foreach (string postRollSequence in context.GetPostRollSequence()) { context.PushFillerKind(FillerKind.PostRoll); await executeSequence(postRollSequence); context.PopFillerKind(); } } if (!stopBeforeEnd) { return context.CurrentTime; } return offlineTail ? targetTime : context.CurrentTime; } }