using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Scheduling.BlockScheduling; using ErsatzTV.Core.Scheduling.YamlScheduling.Models; using Microsoft.Extensions.Logging; namespace ErsatzTV.Core.Scheduling.YamlScheduling; public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepository, ILogger logger) { private readonly Dictionary _enumerators = new(); private readonly Dictionary> _mediaItems = new(); private readonly Dictionary> _playlistMediaItems = new(); public System.Collections.Generic.HashSet MissingContentKeys { get; } = []; public List MediaItemsForContent(string contentKey) => _mediaItems.TryGetValue(contentKey, out List items) ? items : []; public List PlaylistMediaItemsForContent(string contentKey, CollectionKey collectionKey) => _playlistMediaItems.TryGetValue(new PlaylistKey(contentKey, collectionKey), out List items) ? items : []; 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, string.Empty); break; case YamlPlayoutContentShowItem show: items = await mediaCollectionRepository.GetShowItemsByShowGuids( show.Guids.Map(g => $"{g.Source}://{g.Value}").ToList()); break; case YamlPlayoutContentCollectionItem collection: items = await mediaCollectionRepository.GetCollectionItemsByName(collection.Collection); break; case YamlPlayoutContentSmartCollectionItem smartCollection: items = await mediaCollectionRepository.GetSmartCollectionItemsByName(smartCollection.SmartCollection); break; case YamlPlayoutContentMultiCollectionItem multiCollection: items = await mediaCollectionRepository.GetMultiCollectionItemsByName(multiCollection.MultiCollection); break; // playlist is handled later } _mediaItems[content.Key] = items; var state = new CollectionEnumeratorState { Seed = context.Playout.Seed + index, Index = 0 }; // marathon is a special case that needs to be handled on its own if (content is YamlPlayoutContentMarathonItem marathon) { var helper = new YamlPlayoutMarathonHelper(mediaCollectionRepository); Option maybeResult = await helper.GetEnumerator( marathon, state, cancellationToken); foreach (YamlMarathonContentResult result in maybeResult) { foreach ((CollectionKey collectionKey, List mediaItems) in result.Content) { _playlistMediaItems.Add(new PlaylistKey(contentKey, collectionKey), mediaItems); } return Some(result.PlaylistEnumerator); } } // playlist is a special case that needs to be handled on its own if (content is YamlPlayoutContentPlaylistItem playlist) { if (!string.IsNullOrWhiteSpace(playlist.Order) && !string.Equals( playlist.Order, "none", StringComparison.OrdinalIgnoreCase)) { logger.LogWarning( "Ignoring playback order {Order} for playlist {Playlist}", playlist.Order, playlist.Playlist); } Dictionary> itemMap = await mediaCollectionRepository.GetPlaylistItemMap(playlist.PlaylistGroup, playlist.Playlist); foreach ((PlaylistItem playlistItem, List mediaItems) in itemMap) { _playlistMediaItems.Add( new PlaylistKey(contentKey, CollectionKey.ForPlaylistItem(playlistItem)), mediaItems); } return await PlaylistEnumerator.Create( mediaCollectionRepository, itemMap, state, false, cancellationToken); } switch (Enum.Parse(content.Order, true)) { case PlaybackOrder.Chronological: return new ChronologicalMediaCollectionEnumerator(items, state); case PlaybackOrder.Shuffle: bool keepMultiPartEpisodesTogether = content.MultiPart; List groupedMediaItems = keepMultiPartEpisodesTogether ? MultiPartEpisodeGrouper.GroupMediaItems(items, false) : items.Map(mi => new GroupedMediaItem(mi, null)).ToList(); return new BlockPlayoutShuffledMediaCollectionEnumerator(groupedMediaItems, state); } return Option.None; } private record PlaylistKey(string ContentKey, CollectionKey CollectionKey); }