diff --git a/ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs b/ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs index db4cdab72..4c473b89a 100644 --- a/ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs +++ b/ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Scheduling; using ErsatzTV.Core.Interfaces.Scheduling; @@ -5,13 +6,13 @@ using ErsatzTV.Core.Scheduling; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Extensions; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging.Abstractions; namespace ErsatzTV.Application.Scheduling; +[SuppressMessage("ReSharper", "SuggestBaseTypeForParameterInConstructor")] public class PreviewBlockPlayoutHandler( IDbContextFactory dbContextFactory, - IBlockPlayoutBuilder blockPlayoutBuilder) + IBlockPlayoutPreviewBuilder blockPlayoutBuilder) : IRequestHandler> { public async Task> Handle( @@ -55,13 +56,7 @@ public class PreviewBlockPlayoutHandler( ] }; - await blockPlayoutBuilder.Build( - playout, - PlayoutBuildMode.Reset, - NullLogger.Instance, - 1, - randomizeStartPoints: true, - cancellationToken); + await blockPlayoutBuilder.Build(playout, PlayoutBuildMode.Reset, cancellationToken); // load playout item details for title foreach (PlayoutItem playoutItem in playout.Items) diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs b/ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs index 468541aba..8b65af361 100644 --- a/ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs @@ -1,18 +1,9 @@ using ErsatzTV.Core.Domain; using ErsatzTV.Core.Scheduling; -using Microsoft.Extensions.Logging; namespace ErsatzTV.Core.Interfaces.Scheduling; public interface IBlockPlayoutBuilder { Task Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken); - - Task Build( - Playout playout, - PlayoutBuildMode mode, - ILogger customLogger, - int daysToBuild, - bool randomizeStartPoints, - CancellationToken cancellationToken); } diff --git a/ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutPreviewBuilder.cs b/ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutPreviewBuilder.cs new file mode 100644 index 000000000..c40d6e3c3 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutPreviewBuilder.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Core.Interfaces.Scheduling; + +public interface IBlockPlayoutPreviewBuilder : IBlockPlayoutBuilder; diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs index bdf279b64..9364f6663 100644 --- a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs @@ -19,24 +19,7 @@ public class BlockPlayoutBuilder( { public async Task Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) { - int daysToBuild = await configElementRepository.GetValue(ConfigElementKey.PlayoutDaysToBuild) - .IfNoneAsync(2); - - return await Build(playout, mode, logger, daysToBuild, randomizeStartPoints: false, cancellationToken); - } - - public async Task Build( - Playout playout, - PlayoutBuildMode mode, - ILogger customLogger, - int daysToBuild, - bool randomizeStartPoints, - CancellationToken cancellationToken) - { - // ReSharper disable once LocalVariableHidesPrimaryConstructorParameter - ILogger log = customLogger ?? logger; - - log.LogDebug( + Logger.LogDebug( "Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}", playout.Id, playout.Channel.Number, @@ -51,6 +34,7 @@ public class BlockPlayoutBuilder( DateTimeOffset start = DateTimeOffset.Now; + int daysToBuild = await GetDaysToBuild(); // get blocks to schedule List blocksToSchedule = EffectiveBlock.GetEffectiveBlocks(playout, start, daysToBuild); @@ -80,14 +64,14 @@ public class BlockPlayoutBuilder( { currentTime = effectiveBlock.Start; - log.LogDebug( + Logger.LogDebug( "Will schedule block {Block} at {Start}", effectiveBlock.Block.Name, effectiveBlock.Start); } else { - log.LogDebug( + Logger.LogDebug( "Will schedule block {Block} with start {Start} at {ActualStart}", effectiveBlock.Block.Name, effectiveBlock.Start, @@ -106,7 +90,7 @@ public class BlockPlayoutBuilder( if (currentTime >= blockFinish) { - log.LogDebug( + Logger.LogDebug( "Current time {Time} for block {Block} is beyond block finish {Finish}; will stop with this block's items", currentTime, effectiveBlock.Block.Name, @@ -119,54 +103,16 @@ public class BlockPlayoutBuilder( string historyKey = HistoryDetails.KeyForBlockItem(blockItem); //logger.LogDebug("History key for block item {Item} is {Key}", blockItem.Id, historyKey); - DateTime historyTime = currentTime.UtcDateTime; - Option maybeHistory = playout.PlayoutHistory - .Filter(h => h.BlockId == blockItem.BlockId) - .Filter(h => h.Key == historyKey) - .Filter(h => h.When < historyTime) - .OrderByDescending(h => h.When) - .HeadOrNone(); - - var state = new CollectionEnumeratorState { Seed = 0, Index = 0 }; - - var collectionKey = CollectionKey.ForBlockItem(blockItem); - List collectionItems = collectionMediaItems[collectionKey]; - - // get enumerator - IMediaCollectionEnumerator enumerator = blockItem.PlaybackOrder switch - { - PlaybackOrder.Chronological => new ChronologicalMediaCollectionEnumerator(collectionItems, state), - PlaybackOrder.SeasonEpisode => new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state), - _ => new RandomizedMediaCollectionEnumerator( - collectionItems, - new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 }) - }; - - // seek to the appropriate place in the collection enumerator - foreach (PlayoutHistory history in maybeHistory) - { - log.LogDebug("History is applicable: {When}: {History}", history.When, history.Details); - - HistoryDetails.MoveToNextItem( - collectionItems, - history.Details, - enumerator, - blockItem.PlaybackOrder); - } - - if (maybeHistory.IsNone && randomizeStartPoints) - { - enumerator.ResetState( - new CollectionEnumeratorState - { - Seed = new Random().Next(), - Index = new Random().Next(collectionItems.Count) - }); - } + IMediaCollectionEnumerator enumerator = GetEnumerator( + playout, + blockItem, + currentTime, + historyKey, + collectionMediaItems); foreach (MediaItem mediaItem in enumerator.Current) { - log.LogDebug( + Logger.LogDebug( "current item: {Id} / {Title}", mediaItem.Id, mediaItem is Episode e ? GetTitle(e) : string.Empty); @@ -217,6 +163,60 @@ public class BlockPlayoutBuilder( return playout; } + protected virtual ILogger Logger => logger; + + protected virtual async Task GetDaysToBuild() + { + return await configElementRepository + .GetValue(ConfigElementKey.PlayoutDaysToBuild) + .IfNoneAsync(2); + } + + protected virtual IMediaCollectionEnumerator GetEnumerator( + Playout playout, + BlockItem blockItem, + DateTimeOffset currentTime, + string historyKey, + Map> collectionMediaItems) + { + DateTime historyTime = currentTime.UtcDateTime; + Option maybeHistory = playout.PlayoutHistory + .Filter(h => h.BlockId == blockItem.BlockId) + .Filter(h => h.Key == historyKey) + .Filter(h => h.When < historyTime) + .OrderByDescending(h => h.When) + .HeadOrNone(); + + var state = new CollectionEnumeratorState { Seed = 0, Index = 0 }; + + var collectionKey = CollectionKey.ForBlockItem(blockItem); + List collectionItems = collectionMediaItems[collectionKey]; + + // get enumerator + IMediaCollectionEnumerator enumerator = blockItem.PlaybackOrder switch + { + PlaybackOrder.Chronological => new ChronologicalMediaCollectionEnumerator(collectionItems, state), + PlaybackOrder.SeasonEpisode => new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state), + _ => new RandomizedMediaCollectionEnumerator( + collectionItems, + new CollectionEnumeratorState { Seed = new Random().Next(), Index = 0 }) + }; + + // seek to the appropriate place in the collection enumerator + foreach (PlayoutHistory history in maybeHistory) + { + Logger.LogDebug("History is applicable: {When}: {History}", history.When, history.Details); + + HistoryDetails.MoveToNextItem( + collectionItems, + history.Details, + enumerator, + blockItem.PlaybackOrder); + } + + return enumerator; + } + private static string GetTitle(Episode e) { string showTitle = e.Season.Show.ShowMetadata.HeadOrNone() diff --git a/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs new file mode 100644 index 000000000..52f6b2e27 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs @@ -0,0 +1,46 @@ +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Scheduling; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace ErsatzTV.Core.Scheduling.BlockScheduling; + +public class BlockPlayoutPreviewBuilder( + IConfigElementRepository configElementRepository, + IMediaCollectionRepository mediaCollectionRepository, + ITelevisionRepository televisionRepository, + IArtistRepository artistRepository, + ILogger logger) : BlockPlayoutBuilder( + configElementRepository, + mediaCollectionRepository, + televisionRepository, + artistRepository, + logger), IBlockPlayoutPreviewBuilder +{ + protected override ILogger Logger => NullLogger.Instance; + + protected override Task GetDaysToBuild() => Task.FromResult(1); + + protected override IMediaCollectionEnumerator GetEnumerator( + Playout playout, + BlockItem blockItem, + DateTimeOffset currentTime, + string historyKey, + Map> collectionMediaItems) + { + IMediaCollectionEnumerator enumerator = base.GetEnumerator(playout, blockItem, currentTime, historyKey, collectionMediaItems); + + var collectionKey = CollectionKey.ForBlockItem(blockItem); + + enumerator.ResetState( + new CollectionEnumeratorState + { + Seed = new Random().Next(), + Index = new Random().Next(collectionMediaItems[collectionKey].Count) + }); + + return enumerator; + } +} diff --git a/ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.cs b/ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.cs index 0eac8364c..f505300bf 100644 --- a/ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.cs +++ b/ErsatzTV.Infrastructure.MySql/Migrations/20240114104626_Fix_BlockMinutes.cs @@ -10,7 +10,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql(@"UPDATE Block SET Minutes = CAST(CEILING(Minutes / 15.0) * 15 AS INT)"); + migrationBuilder.Sql(@"UPDATE Block SET Minutes = CAST(CEILING(Minutes / 15.0) * 15 AS UNSIGNED)"); } /// diff --git a/ErsatzTV/Pages/PlayoutTemplatesEditor.razor b/ErsatzTV/Pages/PlayoutTemplatesEditor.razor index c19975ab0..047a01574 100644 --- a/ErsatzTV/Pages/PlayoutTemplatesEditor.razor +++ b/ErsatzTV/Pages/PlayoutTemplatesEditor.razor @@ -70,8 +70,7 @@ + OnClick="@(_ => DeleteTemplate(context))"> @@ -424,6 +423,12 @@ private async Task SaveChanges() { + if (_items.Any(i => i.Template is null)) + { + Snackbar.Add("Unable to save; item has no template selected", Severity.Error); + return; + } + var items = _items.Map(item => new ReplacePlayoutTemplate( item.Id, item.Index, @@ -455,7 +460,7 @@ _previewItems.Clear(); - var prioritized = _items.OrderBy(t => t.Index).ToList(); + var prioritized = _items.Filter(i => i.Template is not null).OrderBy(t => t.Index).ToList(); if (dateRange.Start.HasValue && dateRange.End.HasValue) { diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index de0c75c15..9a0114fdb 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -653,6 +653,7 @@ public class Startup services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped();