diff --git a/CHANGELOG.md b/CHANGELOG.md index c31d6286b..22e7e506c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix infinite loop caused by impossible schedule (all collection items longer than schedule item duration) - Fix selecting audio and subtitle streams with two-letter language codes - Fix adding pad filler to content that is less than one minute in duration +- Generate unique identifier for virtual HDHomeRun tuner by @raknam + - This allows a single Plex server to connect to multiple ETV instances ### Changed - Remove some unnecessary API calls related to media server scanning and paging diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs new file mode 100644 index 000000000..9af63d610 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs @@ -0,0 +1,85 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling; + +public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepository) +{ + private readonly Dictionary _enumerators = new(); + + public System.Collections.Generic.HashSet MissingContentKeys { get; } = []; + + public async Task> GetCachedEnumeratorForContent( + YamlPlayoutContext context, + string contentKey, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(contentKey)) + { + return Option.None; + } + + if (!_enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator)) + { + Option maybeEnumerator = + await GetEnumeratorForContent(context, contentKey, cancellationToken); + + if (maybeEnumerator.IsNone) + { + return Option.None; + } + + foreach (IMediaCollectionEnumerator e in maybeEnumerator) + { + enumerator = e; + _enumerators.Add(contentKey, enumerator); + } + } + + return Some(enumerator); + } + + private async Task> GetEnumeratorForContent( + YamlPlayoutContext context, + string contentKey, + CancellationToken cancellationToken) + { + int index = context.Definition.Content.FindIndex(c => c.Key == contentKey); + if (index < 0) + { + return Option.None; + } + + List items = []; + + YamlPlayoutContentItem content = context.Definition.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; + } + + // start at the appropriate place in the enumerator + context.ContentIndex.TryGetValue(contentKey, out int enumeratorIndex); + + var state = new CollectionEnumeratorState { Seed = context.Playout.Seed + index, Index = enumeratorIndex }; + 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; + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/IYamlPlayoutHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/IYamlPlayoutHandler.cs new file mode 100644 index 000000000..e1fea8d63 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/IYamlPlayoutHandler.cs @@ -0,0 +1,15 @@ +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public interface IYamlPlayoutHandler +{ + bool Reset { get; } + + Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken); +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs new file mode 100644 index 000000000..24588d53f --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs @@ -0,0 +1,65 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Filler; +using ErsatzTV.Core.Extensions; +using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) : IYamlPlayoutHandler +{ + public bool Reset => false; + + public abstract Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken); + + protected async Task> GetContentEnumerator( + YamlPlayoutContext context, + string contentKey, + ILogger logger, + CancellationToken cancellationToken) + { + Option maybeEnumerator = await enumeratorCache.GetCachedEnumeratorForContent( + context, + contentKey, + cancellationToken); + + if (maybeEnumerator.IsNone) + { + if (!enumeratorCache.MissingContentKeys.Contains(contentKey)) + { + logger.LogWarning("Unable to locate content with key {Key}", contentKey); + enumeratorCache.MissingContentKeys.Add(contentKey); + } + } + + return maybeEnumerator; + } + + protected static TimeSpan DurationForMediaItem(MediaItem mediaItem) + { + if (mediaItem is Image image) + { + return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); + } + + MediaVersion version = mediaItem.GetHeadVersion(); + return version.Duration; + } + + protected static FillerKind GetFillerKind(YamlPlayoutInstruction instruction) + { + if (string.IsNullOrWhiteSpace(instruction.FillerKind)) + { + return FillerKind.None; + } + + return Enum.TryParse(instruction.FillerKind, ignoreCase: true, out FillerKind result) + ? result + : FillerKind.None; + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs new file mode 100644 index 000000000..412313157 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs @@ -0,0 +1,70 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlayoutContentHandler(enumeratorCache) +{ + public override async Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken) + { + if (instruction is not YamlPlayoutCountInstruction count) + { + return false; + } + + Option maybeEnumerator = await GetContentEnumerator( + context, + instruction.Content, + logger, + cancellationToken); + + foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) + { + for (var i = 0; i < count.Count; i++) + { + foreach (MediaItem mediaItem in enumerator.Current) + { + TimeSpan itemDuration = DurationForMediaItem(mediaItem); + + // create a playout item + var playoutItem = new PlayoutItem + { + MediaItemId = mediaItem.Id, + Start = context.CurrentTime.UtcDateTime, + Finish = context.CurrentTime.UtcDateTime + itemDuration, + InPoint = TimeSpan.Zero, + OutPoint = itemDuration, + FillerKind = GetFillerKind(count), + //CustomTitle = scheduleItem.CustomTitle, + //WatermarkId = scheduleItem.WatermarkId, + //PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, + //PreferredAudioTitle = scheduleItem.PreferredAudioTitle, + //PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, + //SubtitleMode = scheduleItem.SubtitleMode + GuideGroup = context.GuideGroup + //GuideStart = effectiveBlock.Start.UtcDateTime, + //GuideFinish = blockFinish.UtcDateTime, + //BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey), + //CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings), + //CollectionEtag = collectionEtags[collectionKey] + }; + + context.Playout.Items.Add(playoutItem); + + context.CurrentTime += itemDuration; + enumerator.MoveNext(); + } + } + + return true; + } + + return false; + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerDuration.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs similarity index 58% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerDuration.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs index ae59ac32c..b4198c7d2 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerDuration.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs @@ -1,53 +1,73 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; using TimeSpanParserUtil; -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; -public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler +public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlPlayoutContentHandler(enumeratorCache) { - public static DateTimeOffset Schedule( - Playout playout, - DateTimeOffset currentTime, - int guideGroup, - YamlPlayoutDurationInstruction duration, - IMediaCollectionEnumerator enumerator, - Option fallbackEnumerator) + public override async Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + 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 currentTime; + return false; } - DateTimeOffset targetTime = currentTime.Add(timeSpan); + DateTimeOffset targetTime = context.CurrentTime.Add(timeSpan); - return Schedule( - playout, - currentTime, - targetTime, - duration.DiscardAttempts, - duration.Trim, - GetFillerKind(duration), - guideGroup, - enumerator, - fallbackEnumerator); + Option maybeEnumerator = await GetContentEnumerator( + context, + instruction.Content, + logger, + cancellationToken); + + Option fallbackEnumerator = await GetContentEnumerator( + context, + duration.Fallback, + logger, + cancellationToken); + + foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) + { + context.CurrentTime = Schedule( + context, + targetTime, + duration.DiscardAttempts, + duration.Trim, + GetFillerKind(duration), + enumerator, + fallbackEnumerator); + + return true; + } + + return false; } protected static DateTimeOffset Schedule( - Playout playout, - DateTimeOffset currentTime, + YamlPlayoutContext context, DateTimeOffset targetTime, int discardAttempts, bool trim, FillerKind fillerKind, - int guideGroup, IMediaCollectionEnumerator enumerator, Option fallbackEnumerator) { bool done = false; - TimeSpan remainingToFill = targetTime - currentTime; + TimeSpan remainingToFill = targetTime - context.CurrentTime; while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero) { foreach (MediaItem mediaItem in enumerator.Current) @@ -57,11 +77,11 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler var playoutItem = new PlayoutItem { MediaItemId = mediaItem.Id, - Start = currentTime.UtcDateTime, - Finish = currentTime.UtcDateTime + itemDuration, + Start = context.CurrentTime.UtcDateTime, + Finish = context.CurrentTime.UtcDateTime + itemDuration, InPoint = TimeSpan.Zero, OutPoint = itemDuration, - GuideGroup = guideGroup, + GuideGroup = context.GuideGroup, FillerKind = fillerKind //DisableWatermarks = !allowWatermarks }; @@ -69,9 +89,9 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler if (remainingToFill - itemDuration >= TimeSpan.Zero) { remainingToFill -= itemDuration; - currentTime += itemDuration; + context.CurrentTime += itemDuration; - playout.Items.Add(playoutItem); + context.Playout.Items.Add(playoutItem); enumerator.MoveNext(); } else if (discardAttempts > 0) @@ -84,12 +104,12 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler { // trim item to exactly fit remainingToFill = TimeSpan.Zero; - currentTime = targetTime; + context.CurrentTime = targetTime; playoutItem.Finish = targetTime.UtcDateTime; playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start; - playout.Items.Add(playoutItem); + context.Playout.Items.Add(playoutItem); enumerator.MoveNext(); } else if (fallbackEnumerator.IsSome) @@ -106,7 +126,7 @@ public class YamlPlayoutSchedulerDuration : YamlPlayoutScheduler playoutItem.Finish = targetTime.UtcDateTime; playoutItem.FillerKind = FillerKind.Fallback; - playout.Items.Add(playoutItem); + context.Playout.Items.Add(playoutItem); fallback.MoveNext(); } } diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutNewEpgGroupHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutNewEpgGroupHandler.cs new file mode 100644 index 000000000..80a0d4fe8 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutNewEpgGroupHandler.cs @@ -0,0 +1,24 @@ +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public class YamlPlayoutNewEpgGroupHandler : IYamlPlayoutHandler +{ + public bool Reset => false; + + public Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken) + { + if (instruction is not YamlPlayoutNewEpgGroupInstruction) + { + return Task.FromResult(false); + } + + context.GuideGroup *= -1; + return Task.FromResult(true); + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs new file mode 100644 index 000000000..b631f7dec --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs @@ -0,0 +1,68 @@ +using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : YamlPlayoutDurationHandler(enumeratorCache) +{ + public override async Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken) + { + if (instruction is not YamlPlayoutPadToNextInstruction padToNext) + { + return false; + } + + int currentMinute = context.CurrentTime.Minute; + + int targetMinute = (currentMinute + padToNext.PadToNext - 1) / padToNext.PadToNext * padToNext.PadToNext; + + DateTimeOffset almostTargetTime = + context.CurrentTime - TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(targetMinute); + + var targetTime = new DateTimeOffset( + almostTargetTime.Year, + almostTargetTime.Month, + almostTargetTime.Day, + almostTargetTime.Hour, + almostTargetTime.Minute, + 0, + almostTargetTime.Offset); + + // ensure filler works for content less than one minute + if (targetTime <= context.CurrentTime) + targetTime = targetTime.AddMinutes(padToNext.PadToNext); + + Option maybeEnumerator = await GetContentEnumerator( + context, + instruction.Content, + logger, + cancellationToken); + + Option fallbackEnumerator = await GetContentEnumerator( + context, + padToNext.Fallback, + logger, + cancellationToken); + + foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) + { + context.CurrentTime = Schedule( + context, + targetTime, + padToNext.DiscardAttempts, + padToNext.Trim, + GetFillerKind(padToNext), + enumerator, + fallbackEnumerator); + + return true; + } + + return false; + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs new file mode 100644 index 000000000..393a2161a --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs @@ -0,0 +1,33 @@ +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler +{ + private int _itemsSinceLastRepeat; + + public bool Reset => false; + + public Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken) + { + if (instruction is not YamlPlayoutRepeatInstruction) + { + return Task.FromResult(false); + } + + if (_itemsSinceLastRepeat == context.Playout.Items.Count) + { + logger.LogWarning("Repeat encountered without adding any playout items; aborting"); + return Task.FromResult(false); + } + + _itemsSinceLastRepeat = context.Playout.Items.Count; + context.InstructionIndex = 0; + return Task.FromResult(true); + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs new file mode 100644 index 000000000..da73dd7ba --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs @@ -0,0 +1,33 @@ +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public class YamlPlayoutSkipItemsHandler : IYamlPlayoutHandler +{ + public bool Reset => true; + + public Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken) + { + if (instruction is not YamlPlayoutSkipItemsInstruction skipItems) + { + return Task.FromResult(false); + } + + if (context.ContentIndex.TryGetValue(skipItems.Content, out int value)) + { + value += skipItems.SkipItems; + } + else + { + value = skipItems.SkipItems; + } + + context.ContentIndex[skipItems.Content] = value; + return Task.FromResult(true); + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWaitUntilHandler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWaitUntilHandler.cs new file mode 100644 index 000000000..b68e5dfa9 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWaitUntilHandler.cs @@ -0,0 +1,47 @@ +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; +using Microsoft.Extensions.Logging; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; + +public class YamlPlayoutWaitUntilHandler : IYamlPlayoutHandler +{ + public bool Reset => true; + + public Task Handle( + YamlPlayoutContext context, + YamlPlayoutInstruction instruction, + ILogger logger, + CancellationToken cancellationToken) + { + if (instruction is not YamlPlayoutWaitUntilInstruction waitUntil) + { + return Task.FromResult(false); + } + + DateTimeOffset currentTime = context.CurrentTime; + + if (TimeOnly.TryParse(waitUntil.WaitUntil, out TimeOnly result)) + { + var dayOnly = DateOnly.FromDateTime(currentTime.LocalDateTime); + var timeOnly = TimeOnly.FromDateTime(currentTime.LocalDateTime); + + if (timeOnly > result) + { + if (waitUntil.Tomorrow) + { + // this is wrong when offset changes + dayOnly = dayOnly.AddDays(1); + currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset); + } + } + else + { + // this is wrong when offset changes + currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset); + } + } + + context.CurrentTime = currentTime; + return Task.FromResult(true); + } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentGuid.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentGuid.cs similarity index 67% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentGuid.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentGuid.cs index 5579ae9b6..b11d622a2 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentGuid.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentGuid.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutContentGuid { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentItem.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs similarity index 66% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentItem.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs index 48a967475..e27788961 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentItem.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentItem.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutContentItem { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentSearchItem.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSearchItem.cs similarity index 71% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentSearchItem.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSearchItem.cs index ddee1ac5e..cb1a1a438 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentSearchItem.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSearchItem.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutContentSearchItem : YamlPlayoutContentItem { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentShowItem.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentShowItem.cs similarity index 74% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentShowItem.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentShowItem.cs index 9c0e3eacc..adcf42f79 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContentShowItem.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentShowItem.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutContentShowItem : YamlPlayoutContentItem { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutCountInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutCountInstruction.cs similarity index 64% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutCountInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutCountInstruction.cs index 89353c989..1aeb40460 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutCountInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutCountInstruction.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutCountInstruction : YamlPlayoutInstruction { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDefinition.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDefinition.cs similarity index 80% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDefinition.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDefinition.cs index b2544e418..dcb5991e2 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDefinition.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDefinition.cs @@ -1,4 +1,4 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutDefinition { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDurationInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDurationInstruction.cs similarity index 85% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDurationInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDurationInstruction.cs index d250e74ae..1e42992fd 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutDurationInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutDurationInstruction.cs @@ -1,6 +1,6 @@ using YamlDotNet.Serialization; -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutDurationInstruction : YamlPlayoutInstruction { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutInstruction.cs similarity index 68% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutInstruction.cs index 89ac5d300..30007f2ad 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutInstruction.cs @@ -1,9 +1,11 @@ using YamlDotNet.Serialization; -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutInstruction { + public virtual bool ChangesIndex => false; + public string Content { get; set; } [YamlMember(Alias = "filler_kind", ApplyNamingConventions = false)] diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutNewEpgGroupInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutNewEpgGroupInstruction.cs similarity index 79% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutNewEpgGroupInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutNewEpgGroupInstruction.cs index 3e10492ea..1261d4330 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutNewEpgGroupInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutNewEpgGroupInstruction.cs @@ -1,6 +1,6 @@ using YamlDotNet.Serialization; -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutNewEpgGroupInstruction : YamlPlayoutInstruction { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutPadToNextInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutPadToNextInstruction.cs similarity index 87% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutPadToNextInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutPadToNextInstruction.cs index 1ce0a613b..b52bf638f 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutPadToNextInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutPadToNextInstruction.cs @@ -1,6 +1,6 @@ using YamlDotNet.Serialization; -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutPadToNextInstruction : YamlPlayoutInstruction { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutRepeatInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutRepeatInstruction.cs similarity index 50% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutRepeatInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutRepeatInstruction.cs index 89c2675b6..2d246ba40 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutRepeatInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutRepeatInstruction.cs @@ -1,6 +1,8 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutRepeatInstruction : YamlPlayoutInstruction { + public override bool ChangesIndex => true; + public bool Repeat { get; set; } } diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSkipItemsInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSkipItemsInstruction.cs similarity index 78% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSkipItemsInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSkipItemsInstruction.cs index 1518207d8..fbe2ad841 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSkipItemsInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutSkipItemsInstruction.cs @@ -1,6 +1,6 @@ using YamlDotNet.Serialization; -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutSkipItemsInstruction : YamlPlayoutInstruction { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutWaitUntilInstruction.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutWaitUntilInstruction.cs similarity index 81% rename from ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutWaitUntilInstruction.cs rename to ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutWaitUntilInstruction.cs index f93a971ec..3d042d9e7 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutWaitUntilInstruction.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutWaitUntilInstruction.cs @@ -1,6 +1,6 @@ using YamlDotNet.Serialization; -namespace ErsatzTV.Core.Scheduling.YamlScheduling; +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; public class YamlPlayoutWaitUntilInstruction : YamlPlayoutInstruction { diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs index 06080f405..3990fbafa 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs @@ -2,6 +2,8 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; +using ErsatzTV.Core.Scheduling.YamlScheduling.Handlers; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using Microsoft.Extensions.Logging; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -35,263 +37,112 @@ public class YamlPlayoutBuilder( return playout; } - YamlPlayoutInitialState initialState = HandleResetActions(playout, playoutDefinition, start); - - DateTimeOffset currentTime = initialState.CurrentTime; - // load content and content enumerators on demand - Dictionary enumerators = new(); - System.Collections.Generic.HashSet missingContentKeys = []; + Dictionary handlers = new(); + var enumeratorCache = new EnumeratorCache(mediaCollectionRepository); - int itemsAfterRepeat = playout.Items.Count; - int guideGroup = 1; - var index = 0; - while (currentTime < finish) + var context = new YamlPlayoutContext(playout, playoutDefinition) { - if (index >= playoutDefinition.Playout.Count) + CurrentTime = start, + GuideGroup = 1, + InstructionIndex = 0 + }; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (mode is PlayoutBuildMode.Reset) + { + context.Playout.Seed = new Random().Next(); + context.Playout.Items.Clear(); + + // handle all on-reset instructions + foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset) + { + Option maybeHandler = GetHandlerForInstruction( + handlers, + enumeratorCache, + instruction); + + foreach (IYamlPlayoutHandler handler in maybeHandler) + { + if (!handler.Reset) + { + logger.LogInformation( + "Skipping unsupported reset instruction {Instruction}", + instruction.GetType().Name); + } + else + { + await handler.Handle(context, instruction, logger, cancellationToken); + } + } + } + } + + // handle all playout instructions + while (context.CurrentTime < finish) + { + if (context.InstructionIndex >= playoutDefinition.Playout.Count) { logger.LogInformation("Reached the end of the YAML playout definition; stopping"); break; } - YamlPlayoutInstruction playoutItem = playoutDefinition.Playout[index]; + YamlPlayoutInstruction instruction = playoutDefinition.Playout[context.InstructionIndex]; + Option maybeHandler = GetHandlerForInstruction(handlers, enumeratorCache, instruction); - // handle instructions that don't reference content - switch (playoutItem) + foreach (IYamlPlayoutHandler handler in maybeHandler) { - case YamlPlayoutWaitUntilInstruction waitUntil: - currentTime = HandleWaitUntil(currentTime, waitUntil); - index++; - continue; - case YamlPlayoutRepeatInstruction: - // repeat resets index into YAML playout - index = 0; - if (playout.Items.Count == itemsAfterRepeat) - { - logger.LogWarning("Repeat encountered without adding any playout items; aborting"); - break; - } - - itemsAfterRepeat = playout.Items.Count; - continue; - case YamlPlayoutNewEpgGroupInstruction: - guideGroup *= -1; - index++; - continue; - } - - Option maybeEnumerator = await GetCachedEnumeratorForContent( - initialState, - playout, - playoutDefinition, - enumerators, - playoutItem.Content, - cancellationToken); - - if (maybeEnumerator.IsNone) - { - if (!missingContentKeys.Contains(playoutItem.Content)) + if (!await handler.Handle(context, instruction, logger, cancellationToken)) { - logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); - missingContentKeys.Add(playoutItem.Content); + logger.LogInformation("YAML playout instruction handler failed"); } } - foreach (IMediaCollectionEnumerator enumerator in maybeEnumerator) + if (!instruction.ChangesIndex) { - switch (playoutItem) - { - case YamlPlayoutCountInstruction count: - currentTime = YamlPlayoutSchedulerCount.Schedule(playout, currentTime, guideGroup, count, enumerator); - break; - case YamlPlayoutDurationInstruction duration: - Option durationFallbackEnumerator = await GetCachedEnumeratorForContent( - initialState, - playout, - playoutDefinition, - enumerators, - duration.Fallback, - cancellationToken); - currentTime = YamlPlayoutSchedulerDuration.Schedule( - playout, - currentTime, - guideGroup, - duration, - enumerator, - durationFallbackEnumerator); - break; - case YamlPlayoutPadToNextInstruction padToNext: - Option fallbackEnumerator = await GetCachedEnumeratorForContent( - initialState, - playout, - playoutDefinition, - enumerators, - padToNext.Fallback, - cancellationToken); - currentTime = YamlPlayoutSchedulerPadToNext.Schedule( - playout, - currentTime, - guideGroup, - padToNext, - enumerator, - fallbackEnumerator); - break; - } + context.InstructionIndex++; } - - index++; } return playout; } - private YamlPlayoutInitialState HandleResetActions( - Playout playout, - YamlPlayoutDefinition playoutDefinition, - DateTimeOffset currentTime) - { - var result = new YamlPlayoutInitialState { CurrentTime = currentTime }; - - // these are only for reset - playout.Seed = new Random().Next(); - playout.Items.Clear(); - - foreach (YamlPlayoutInstruction instruction in playoutDefinition.Reset) - { - switch (instruction) - { - case YamlPlayoutWaitUntilInstruction waitUntil: - result.CurrentTime = HandleWaitUntil(result.CurrentTime, waitUntil); - break; - case YamlPlayoutSkipItemsInstruction skipItems: - if (result.ContentIndex.TryGetValue(skipItems.Content, out int value)) - { - value += skipItems.SkipItems; - } - else - { - value = skipItems.SkipItems; - } - - result.ContentIndex[skipItems.Content] = value; - break; - default: - logger.LogInformation( - "Skipping unsupported reset instruction {Instruction}", - instruction.GetType().Name); - break; - } - } - - return result; - } - - private static DateTimeOffset HandleWaitUntil(DateTimeOffset currentTime, YamlPlayoutWaitUntilInstruction waitUntil) - { - if (TimeOnly.TryParse(waitUntil.WaitUntil, out TimeOnly result)) - { - var dayOnly = DateOnly.FromDateTime(currentTime.LocalDateTime); - var timeOnly = TimeOnly.FromDateTime(currentTime.LocalDateTime); - - if (timeOnly > result) - { - if (waitUntil.Tomorrow) - { - // this is wrong when offset changes - dayOnly = dayOnly.AddDays(1); - currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset); - } - } - else - { - // this is wrong when offset changes - currentTime = new DateTimeOffset(dayOnly, result, currentTime.Offset); - } - } - - return currentTime; - } - private async Task GetDaysToBuild() => await configElementRepository .GetValue(ConfigElementKey.PlayoutDaysToBuild) .IfNoneAsync(2); - private async Task> GetCachedEnumeratorForContent( - YamlPlayoutInitialState initialState, - Playout playout, - YamlPlayoutDefinition playoutDefinition, - Dictionary enumerators, - string contentKey, - CancellationToken cancellationToken) + private static Option GetHandlerForInstruction( + Dictionary handlers, + EnumeratorCache enumeratorCache, + YamlPlayoutInstruction instruction) { - if (string.IsNullOrWhiteSpace(contentKey)) + if (handlers.TryGetValue(instruction, out IYamlPlayoutHandler handler)) { - return Option.None; + return Optional(handler); } - if (!enumerators.TryGetValue(contentKey, out IMediaCollectionEnumerator enumerator)) + handler = instruction switch { - Option maybeEnumerator = - await GetEnumeratorForContent(initialState, playout, contentKey, playoutDefinition, cancellationToken); + YamlPlayoutRepeatInstruction => new YamlPlayoutRepeatHandler(), + YamlPlayoutWaitUntilInstruction => new YamlPlayoutWaitUntilHandler(), + YamlPlayoutNewEpgGroupInstruction => new YamlPlayoutNewEpgGroupHandler(), + YamlPlayoutSkipItemsInstruction => new YamlPlayoutSkipItemsHandler(), - if (maybeEnumerator.IsNone) - { - return Option.None; - } + // content handlers + YamlPlayoutCountInstruction => new YamlPlayoutCountHandler(enumeratorCache), + YamlPlayoutDurationInstruction => new YamlPlayoutDurationHandler(enumeratorCache), + YamlPlayoutPadToNextInstruction => new YamlPlayoutPadToNextHandler(enumeratorCache), - foreach (IMediaCollectionEnumerator e in maybeEnumerator) - { - enumerator = e; - enumerators.Add(contentKey, enumerator); - } + _ => null + }; + + if (handler != null) + { + handlers.Add(instruction, handler); } - return Some(enumerator); - } - - private async Task> GetEnumeratorForContent( - YamlPlayoutInitialState initialState, - 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; - } - - // start at the appropriate place in the enumerator - initialState.ContentIndex.TryGetValue(contentKey, out int enumeratorIndex); - - var state = new CollectionEnumeratorState { Seed = playout.Seed + index, Index = enumeratorIndex }; - 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; + return Optional(handler); } private static async Task LoadYamlDefinition(Playout playout, CancellationToken cancellationToken) diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs new file mode 100644 index 000000000..4c392b550 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutContext.cs @@ -0,0 +1,20 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling.YamlScheduling.Models; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling; + +public class YamlPlayoutContext(Playout playout, YamlPlayoutDefinition definition) +{ + public Playout Playout { get; } = playout; + + public YamlPlayoutDefinition Definition { get; } = definition; + + public DateTimeOffset CurrentTime { get; set; } + + public int InstructionIndex { get; set; } + + public int GuideGroup { get; set; } + + // only used for initial state (skip items) + public Dictionary ContentIndex { get; } = []; +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInitialState.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInitialState.cs deleted file mode 100644 index ddab73b81..000000000 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutInitialState.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace ErsatzTV.Core.Scheduling.YamlScheduling; - -public class YamlPlayoutInitialState -{ - public DateTimeOffset CurrentTime { get; set; } - - public Dictionary ContentIndex { get; } = []; -} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutScheduler.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutScheduler.cs deleted file mode 100644 index 8e8a13a37..000000000 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutScheduler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Domain.Filler; -using ErsatzTV.Core.Extensions; - -namespace ErsatzTV.Core.Scheduling.YamlScheduling; - -public abstract class YamlPlayoutScheduler -{ - protected static TimeSpan DurationForMediaItem(MediaItem mediaItem) - { - if (mediaItem is Image image) - { - return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); - } - - MediaVersion version = mediaItem.GetHeadVersion(); - return version.Duration; - } - - protected static FillerKind GetFillerKind(YamlPlayoutInstruction instruction) - { - if (string.IsNullOrWhiteSpace(instruction.FillerKind)) - { - return FillerKind.None; - } - - return Enum.TryParse(instruction.FillerKind, ignoreCase: true, out FillerKind result) - ? result - : FillerKind.None; - } -} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerCount.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerCount.cs deleted file mode 100644 index d77c9f99e..000000000 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerCount.cs +++ /dev/null @@ -1,53 +0,0 @@ -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Scheduling; - -namespace ErsatzTV.Core.Scheduling.YamlScheduling; - -public class YamlPlayoutSchedulerCount : YamlPlayoutScheduler -{ - public static DateTimeOffset Schedule( - Playout playout, - DateTimeOffset currentTime, - int guideGroup, - YamlPlayoutCountInstruction count, - IMediaCollectionEnumerator enumerator) - { - for (int i = 0; i < count.Count; i++) - { - foreach (MediaItem mediaItem in enumerator.Current) - { - TimeSpan itemDuration = DurationForMediaItem(mediaItem); - - // create a playout item - var playoutItem = new PlayoutItem - { - MediaItemId = mediaItem.Id, - Start = currentTime.UtcDateTime, - Finish = currentTime.UtcDateTime + itemDuration, - InPoint = TimeSpan.Zero, - OutPoint = itemDuration, - FillerKind = GetFillerKind(count), - //CustomTitle = scheduleItem.CustomTitle, - //WatermarkId = scheduleItem.WatermarkId, - //PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode, - //PreferredAudioTitle = scheduleItem.PreferredAudioTitle, - //PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode, - //SubtitleMode = scheduleItem.SubtitleMode - GuideGroup = guideGroup - //GuideStart = effectiveBlock.Start.UtcDateTime, - //GuideFinish = blockFinish.UtcDateTime, - //BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey), - //CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings), - //CollectionEtag = collectionEtags[collectionKey] - }; - - playout.Items.Add(playoutItem); - - currentTime += itemDuration; - enumerator.MoveNext(); - } - } - - return currentTime; - } -} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerPadToNext.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerPadToNext.cs deleted file mode 100644 index 770cfba89..000000000 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutSchedulerPadToNext.cs +++ /dev/null @@ -1,47 +0,0 @@ -using ErsatzTV.Core.Domain; -using ErsatzTV.Core.Interfaces.Scheduling; - -namespace ErsatzTV.Core.Scheduling.YamlScheduling; - -public class YamlPlayoutSchedulerPadToNext : YamlPlayoutSchedulerDuration -{ - public static DateTimeOffset Schedule( - Playout playout, - DateTimeOffset currentTime, - int guideGroup, - YamlPlayoutPadToNextInstruction padToNext, - IMediaCollectionEnumerator enumerator, - Option fallbackEnumerator) - { - int currentMinute = currentTime.Minute; - - int targetMinute = (currentMinute + padToNext.PadToNext - 1) / padToNext.PadToNext * padToNext.PadToNext; - - DateTimeOffset almostTargetTime = - currentTime - TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(targetMinute); - - var targetTime = new DateTimeOffset( - almostTargetTime.Year, - almostTargetTime.Month, - almostTargetTime.Day, - almostTargetTime.Hour, - almostTargetTime.Minute, - 0, - almostTargetTime.Offset); - - // ensure filler works for content less than one minute - if (targetTime <= currentTime) - targetTime = targetTime.AddMinutes(padToNext.PadToNext); - - return Schedule( - playout, - currentTime, - targetTime, - padToNext.DiscardAttempts, - padToNext.Trim, - GetFillerKind(padToNext), - guideGroup, - enumerator, - fallbackEnumerator); - } -}