From 912f79097d4a24578d72582a93b8157c5ecf03da Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:46:08 -0500 Subject: [PATCH] add collection, smart collection, multi collection, playlist content sources to yaml playouts (#1841) * add collection content to yaml playout * add smart_collection content * add multi_collection content * add playlist content --- .../Fakes/FakeMediaCollectionRepository.cs | 6 ++ .../IMediaCollectionRepository.cs | 4 ++ .../YamlScheduling/EnumeratorCache.cs | 37 ++++++++++- .../YamlPlayoutContentCollectionItem.cs | 6 ++ .../YamlPlayoutContentMultiCollectionItem.cs | 9 +++ .../Models/YamlPlayoutContentPlaylistItem.cs | 11 ++++ .../YamlPlayoutContentSmartCollectionItem.cs | 9 +++ .../YamlScheduling/YamlPlayoutBuilder.cs | 8 ++- .../Repositories/MediaCollectionRepository.cs | 64 +++++++++++++++++++ 9 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentCollectionItem.cs create mode 100644 ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentMultiCollectionItem.cs create mode 100644 ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentPlaylistItem.cs create mode 100644 ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSmartCollectionItem.cs diff --git a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs index 81278c13..29b8beee 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs @@ -13,6 +13,9 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository public Task>> GetPlaylistItemMap(int playlistId) => throw new NotSupportedException(); + public Task>> GetPlaylistItemMap(string groupName, string name) => + throw new NotSupportedException(); + public Task>> GetPlaylistItemMap(Playlist playlist) => throw new NotSupportedException(); @@ -20,8 +23,11 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository throw new NotSupportedException(); public Task> GetItems(int id) => _data[id].ToList().AsTask(); + public Task> GetCollectionItemsByName(string name) => throw new NotSupportedException(); public Task> GetMultiCollectionItems(int id) => throw new NotSupportedException(); + public Task> GetMultiCollectionItemsByName(string name) => throw new NotSupportedException(); public Task> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask(); + public Task> GetSmartCollectionItemsByName(string name) => throw new NotSupportedException(); public Task> GetSmartCollectionItems(string query) => throw new NotSupportedException(); public Task> GetShowItemsByShowGuids(List guids) => throw new NotSupportedException(); public Task> GetPlaylistItems(int id) => throw new NotSupportedException(); diff --git a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs index 7fa7eb4d..1455805d 100644 --- a/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs +++ b/ErsatzTV.Core/Interfaces/Repositories/IMediaCollectionRepository.cs @@ -6,11 +6,15 @@ namespace ErsatzTV.Core.Interfaces.Repositories; public interface IMediaCollectionRepository { Task>> GetPlaylistItemMap(int playlistId); + Task>> GetPlaylistItemMap(string groupName, string name); Task>> GetPlaylistItemMap(Playlist playlist); Task> GetCollectionWithCollectionItemsUntracked(int id); Task> GetItems(int id); + Task> GetCollectionItemsByName(string name); Task> GetMultiCollectionItems(int id); + Task> GetMultiCollectionItemsByName(string name); Task> GetSmartCollectionItems(int id); + Task> GetSmartCollectionItemsByName(string name); Task> GetSmartCollectionItems(string query); Task> GetShowItemsByShowGuids(List guids); Task> GetPlaylistItems(int id); diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs index d7aaab6c..22103111 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/EnumeratorCache.cs @@ -3,10 +3,11 @@ 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) +public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepository, ILogger logger) { private readonly Dictionary> _mediaItems = new(); private readonly Dictionary _enumerators = new(); @@ -49,7 +50,7 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor private async Task> GetEnumeratorForContent( YamlPlayoutContext context, string contentKey, - CancellationToken _) + CancellationToken cancellationToken) { int index = context.Definition.Content.FindIndex(c => c.Key == contentKey); if (index < 0) @@ -69,11 +70,43 @@ public class EnumeratorCache(IMediaCollectionRepository mediaCollectionRepositor 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 }; + + // playlist is a special case that needs to be handled on its own + if (content is YamlPlayoutContentPlaylistItem playlist) + { + if (!string.IsNullOrWhiteSpace(playlist.Order)) + { + logger.LogWarning( + "Ignoring playback order {Order} for playlist {Playlist}", + playlist.Order, + playlist.Playlist); + } + + Dictionary> itemMap = + await mediaCollectionRepository.GetPlaylistItemMap(playlist.PlaylistGroup, playlist.Playlist); + + return await PlaylistEnumerator.Create( + mediaCollectionRepository, + itemMap, + state, + cancellationToken); + } + switch (Enum.Parse(content.Order, true)) { case PlaybackOrder.Chronological: diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentCollectionItem.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentCollectionItem.cs new file mode 100644 index 00000000..ad64db77 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentCollectionItem.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; + +public class YamlPlayoutContentCollectionItem : YamlPlayoutContentItem +{ + public string Collection { get; set; } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentMultiCollectionItem.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentMultiCollectionItem.cs new file mode 100644 index 00000000..025e3cfc --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentMultiCollectionItem.cs @@ -0,0 +1,9 @@ +using YamlDotNet.Serialization; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; + +public class YamlPlayoutContentMultiCollectionItem : YamlPlayoutContentItem +{ + [YamlMember(Alias = "multi_collection", ApplyNamingConventions = false)] + public string MultiCollection { get; set; } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentPlaylistItem.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentPlaylistItem.cs new file mode 100644 index 00000000..f46ccc2a --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentPlaylistItem.cs @@ -0,0 +1,11 @@ +using YamlDotNet.Serialization; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; + +public class YamlPlayoutContentPlaylistItem : YamlPlayoutContentItem +{ + public string Playlist { get; set; } + + [YamlMember(Alias = "playlist_group", ApplyNamingConventions = false)] + public string PlaylistGroup { get; set; } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSmartCollectionItem.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSmartCollectionItem.cs new file mode 100644 index 00000000..29ed5498 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/Models/YamlPlayoutContentSmartCollectionItem.cs @@ -0,0 +1,9 @@ +using YamlDotNet.Serialization; + +namespace ErsatzTV.Core.Scheduling.YamlScheduling.Models; + +public class YamlPlayoutContentSmartCollectionItem : YamlPlayoutContentItem +{ + [YamlMember(Alias = "smart_collection", ApplyNamingConventions = false)] + public string SmartCollection { get; set; } +} diff --git a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs index 049e7bc5..acaff6eb 100644 --- a/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs @@ -35,7 +35,7 @@ public class YamlPlayoutBuilder( DateTimeOffset finish = start.AddDays(daysToBuild); Dictionary handlers = new(); - var enumeratorCache = new EnumeratorCache(mediaCollectionRepository); + var enumeratorCache = new EnumeratorCache(mediaCollectionRepository, logger); var context = new YamlPlayoutContext(playout, playoutDefinition, guideGroup: 1) { @@ -280,8 +280,12 @@ public class YamlPlayoutBuilder( { var contentKeyMappings = new Dictionary { + { "collection", typeof(YamlPlayoutContentCollectionItem) }, + { "multi_collection", typeof(YamlPlayoutContentMultiCollectionItem) }, + { "playlist", typeof(YamlPlayoutContentPlaylistItem) }, { "search", typeof(YamlPlayoutContentSearchItem) }, - { "show", typeof(YamlPlayoutContentShowItem) } + { "show", typeof(YamlPlayoutContentShowItem) }, + { "smart_collection", typeof(YamlPlayoutContentSmartCollectionItem) } }; o.AddUniqueKeyTypeDiscriminator(contentKeyMappings); diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 3e83c4a9..27b2a824 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -155,6 +155,21 @@ public class MediaCollectionRepository : IMediaCollectionRepository return result; } + public async Task>> GetPlaylistItemMap(string groupName, string name) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + + Option maybePlaylist = await dbContext.Playlists + .SelectOneAsync(p => p.Name, p => EF.Functions.Collate(p.Name, TvContext.CaseInsensitiveCollation) == name); + + foreach (Playlist playlist in maybePlaylist) + { + return await GetPlaylistItemMap(playlist.Id); + } + + return []; + } + public async Task>> GetPlaylistItemMap(Playlist playlist) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); @@ -307,6 +322,21 @@ public class MediaCollectionRepository : IMediaCollectionRepository return result.Distinct().ToList(); } + public async Task> GetCollectionItemsByName(string name) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + + Option maybeCollection = await dbContext.Collections + .SelectOneAsync(c => c.Name, c => EF.Functions.Collate(c.Name, TvContext.CaseInsensitiveCollation) == name); + + foreach (Collection collection in maybeCollection) + { + return await GetItems(collection.Id); + } + + return []; + } + public async Task> GetMultiCollectionItems(int id) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); @@ -342,6 +372,23 @@ public class MediaCollectionRepository : IMediaCollectionRepository return result.DistinctBy(x => x.Id).ToList(); } + public async Task> GetMultiCollectionItemsByName(string name) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + + Option maybeCollection = await dbContext.MultiCollections + .SelectOneAsync( + mc => mc.Name, + mc => EF.Functions.Collate(mc.Name, TvContext.CaseInsensitiveCollation) == name); + + foreach (MultiCollection collection in maybeCollection) + { + return await GetMultiCollectionItems(collection.Id); + } + + return []; + } + public async Task> GetSmartCollectionItems(int id) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); @@ -357,6 +404,23 @@ public class MediaCollectionRepository : IMediaCollectionRepository return []; } + public async Task> GetSmartCollectionItemsByName(string name) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(); + + Option maybeCollection = await dbContext.SmartCollections + .SelectOneAsync( + sc => sc.Name, + sc => EF.Functions.Collate(sc.Name, TvContext.CaseInsensitiveCollation) == name); + + foreach (SmartCollection collection in maybeCollection) + { + return await GetSmartCollectionItems(collection.Query); + } + + return []; + } + public async Task> GetSmartCollectionItems(string query) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();