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, 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; } 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 = Schedule( context, instruction.Content, duration.Fallback, targetTime, duration.DiscardAttempts, duration.Trim, duration.OfflineTail, GetFillerKind(duration), duration.CustomTitle, enumerator, fallbackEnumerator); return true; } return false; } protected static DateTimeOffset Schedule( YamlPlayoutContext context, string contentKey, string fallbackContentKey, DateTimeOffset targetTime, int discardAttempts, bool trim, bool offlineTail, FillerKind fillerKind, string customTitle, IMediaCollectionEnumerator enumerator, Option fallbackEnumerator) { bool done = false; TimeSpan remainingToFill = targetTime - context.CurrentTime; while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero) { 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 = !allowWatermarks }; if (remainingToFill - itemDuration >= TimeSpan.Zero) { context.Playout.Items.Add(playoutItem); context.AdvanceGuideGroup(); // create history record Option maybeHistory = GetHistoryForItem( context, contentKey, enumerator, playoutItem, mediaItem); 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 Option maybeHistory = GetHistoryForItem( context, contentKey, enumerator, playoutItem, mediaItem); 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 Option maybeHistory = GetHistoryForItem( context, fallbackContentKey, fallback, playoutItem, mediaItem); foreach (PlayoutHistory history in maybeHistory) { context.Playout.PlayoutHistory.Add(history); } fallback.MoveNext(); } } } else { // item won't fit; we're done done = true; } } } return offlineTail ? targetTime : context.CurrentTime; } }