using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using LanguageExt.UnsafeValueAccess; using Microsoft.Extensions.Logging; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace ErsatzTV.Core.Scheduling.YamlScheduling; public class YamlPlayoutBuilder( ILocalFileSystem localFileSystem, IConfigElementRepository configElementRepository, IMediaCollectionRepository mediaCollectionRepository, ILogger logger) : IYamlPlayoutBuilder { public async Task Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) { if (!localFileSystem.FileExists(playout.TemplateFile)) { logger.LogWarning("YAML playout file {File} does not exist; aborting.", playout.TemplateFile); return playout; } YamlPlayoutDefinition playoutDefinition = await LoadYamlDefinition(playout, cancellationToken); DateTimeOffset start = DateTimeOffset.Now; int daysToBuild = await GetDaysToBuild(); DateTimeOffset finish = start.AddDays(daysToBuild); if (mode is not PlayoutBuildMode.Reset) { logger.LogWarning("YAML playouts can only be reset; ignoring build mode {Mode}", mode.ToString()); return playout; } // these are only for reset playout.Seed = new Random().Next(); playout.Items.Clear(); DateTimeOffset currentTime = start; // load content and content enumerators on demand Dictionary enumerators = new(); System.Collections.Generic.HashSet missingContentKeys = []; int itemsAfterRepeat = playout.Items.Count; var index = 0; while (currentTime < finish) { if (index >= playoutDefinition.Playout.Count) { logger.LogInformation("Reached the end of the YAML playout definition; stopping"); break; } YamlPlayoutInstruction playoutItem = playoutDefinition.Playout[index]; // repeat resets index into YAML playout if (playoutItem is YamlPlayoutRepeatInstruction) { index = 0; if (playout.Items.Count == itemsAfterRepeat) { logger.LogWarning("Repeat encountered without adding any playout items; aborting"); break; } itemsAfterRepeat = playout.Items.Count; continue; } Option maybeEnumerator = await GetCachedEnumeratorForContent( playout, playoutDefinition, enumerators, playoutItem.Content, cancellationToken); if (maybeEnumerator.IsNone) { if (!missingContentKeys.Contains(playoutItem.Content)) { logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); missingContentKeys.Add(playoutItem.Content); } } foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) { switch (playoutItem) { case YamlPlayoutCountInstruction count: currentTime = YamlPlayoutSchedulerCount.Schedule(playout, currentTime, count, enumerator); break; case YamlPlayoutDurationInstruction duration: Option durationFallbackEnumerator = await GetCachedEnumeratorForContent( playout, playoutDefinition, enumerators, duration.Fallback, cancellationToken); currentTime = YamlPlayoutSchedulerDuration.Schedule( playout, currentTime, duration, enumerator, durationFallbackEnumerator); break; case YamlPlayoutPadToNextInstruction padToNext: Option fallbackEnumerator = await GetCachedEnumeratorForContent( playout, playoutDefinition, enumerators, padToNext.Fallback, cancellationToken); currentTime = YamlPlayoutSchedulerPadToNext.Schedule( playout, currentTime, padToNext, enumerator, fallbackEnumerator); break; } } index++; } return playout; } private async Task GetDaysToBuild() => await configElementRepository .GetValue(ConfigElementKey.PlayoutDaysToBuild) .IfNoneAsync(2); private async Task> GetCachedEnumeratorForContent( Playout playout, YamlPlayoutDefinition playoutDefinition, Dictionary enumerators, string contentKey, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(contentKey)) { return Option.None; } if (!enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator)) { Option maybeEnumerator = await GetEnumeratorForContent(playout, contentKey, playoutDefinition, cancellationToken); if (maybeEnumerator.IsNone) { return Option.None; } foreach (IMediaCollectionEnumerator e in maybeEnumerator) { enumerator = maybeEnumerator.ValueUnsafe(); enumerators.Add(contentKey, enumerator); } } return Some(enumerator); } private async Task> GetEnumeratorForContent( Playout playout, string contentKey, YamlPlayoutDefinition playoutDefinition, CancellationToken cancellationToken) { int index = playoutDefinition.Content.FindIndex(c => c.Key == contentKey); if (index < 0) { return Option.None; } List items = []; YamlPlayoutContentItem content = playoutDefinition.Content[index]; switch (content) { case YamlPlayoutContentSearchItem search: items = await mediaCollectionRepository.GetSmartCollectionItems(search.Query); break; case YamlPlayoutContentShowItem show: items = await mediaCollectionRepository.GetShowItemsByShowGuids( show.Guids.Map(g => $"{g.Source}://{g.Value}").ToList()); break; } var state = new CollectionEnumeratorState { Seed = playout.Seed + index, Index = 0 }; switch (Enum.Parse(content.Order, true)) { case PlaybackOrder.Chronological: return new ChronologicalMediaCollectionEnumerator(items, state); case PlaybackOrder.Shuffle: // TODO: fix this var groupedMediaItems = items.Map(mi => new GroupedMediaItem(mi, null)).ToList(); return new ShuffledMediaCollectionEnumerator(groupedMediaItems, state, cancellationToken); } return Option.None; } private static async Task LoadYamlDefinition(Playout playout, CancellationToken cancellationToken) { string yaml = await File.ReadAllTextAsync(playout.TemplateFile, cancellationToken); IDeserializer deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .WithTypeDiscriminatingNodeDeserializer( o => { var contentKeyMappings = new Dictionary { { "search", typeof(YamlPlayoutContentSearchItem) }, { "show", typeof(YamlPlayoutContentShowItem) } }; o.AddUniqueKeyTypeDiscriminator(contentKeyMappings); var instructionKeyMappings = new Dictionary { { "count", typeof(YamlPlayoutCountInstruction) }, { "duration", typeof(YamlPlayoutDurationInstruction) }, { "pad_to_next", typeof(YamlPlayoutPadToNextInstruction) }, { "repeat", typeof(YamlPlayoutRepeatInstruction) } }; o.AddUniqueKeyTypeDiscriminator(instructionKeyMappings); }) .Build(); return deserializer.Deserialize(yaml); } }