From dcbe4837bf14205a93639cd794f5a95725903caf Mon Sep 17 00:00:00 2001 From: Jason Dove <1695733+jasongdove@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:01:21 -0600 Subject: [PATCH] first pass at block scheduling (#1548) * add blocks, block groups * basic block and block item editing * add template groups and basic template editing (name) * add blocks to template calendar * edit playout templates * add calendar preview to playout templates * add basic block playout building * add mysql migration * update changelog --- CHANGELOG.md | 8 + Directory.Build.props | 1 + .../ErsatzTV.Application.csproj.DotSettings | 2 + .../Playouts/Commands/BuildPlayoutHandler.cs | 27 +- .../Commands/CreateBlockPlayoutHandler.cs | 67 + .../Playouts/Commands/CreatePlayout.cs | 3 + .../Playouts/Commands/DeletePlayoutHandler.cs | 4 +- .../Queries/GetProgramScheduleItemsHandler.cs | 2 +- .../Scheduling/BlockGroupViewModel.cs | 3 + .../Scheduling/BlockItemViewModel.cs | 15 + .../Scheduling/BlockViewModel.cs | 3 + .../Scheduling/Commands/CreateBlock.cs | 5 + .../Scheduling/Commands/CreateBlockGroup.cs | 5 + .../Commands/CreateBlockGroupHandler.cs | 31 + .../Scheduling/Commands/CreateBlockHandler.cs | 40 + .../Scheduling/Commands/CreateTemplate.cs | 5 + .../Commands/CreateTemplateGroup.cs | 5 + .../Commands/CreateTemplateGroupHandler.cs | 35 + .../Commands/CreateTemplateHandler.cs | 39 + .../Scheduling/Commands/DeleteBlock.cs | 5 + .../Scheduling/Commands/DeleteBlockGroup.cs | 5 + .../Commands/DeleteBlockGroupHandler.cs | 29 + .../Scheduling/Commands/DeleteBlockHandler.cs | 29 + .../Scheduling/Commands/DeleteTemplate.cs | 5 + .../Commands/DeleteTemplateGroup.cs | 5 + .../Commands/DeleteTemplateGroupHandler.cs | 29 + .../Commands/DeleteTemplateHandler.cs | 29 + .../Scheduling/Commands/ReplaceBlockItem.cs | 12 + .../Scheduling/Commands/ReplaceBlockItems.cs | 6 + .../Commands/ReplaceBlockItemsHandler.cs | 123 + .../Commands/ReplacePlayoutTemplate.cs | 9 + .../Commands/ReplacePlayoutTemplateItems.cs | 6 + .../ReplacePlayoutTemplateItemsHandler.cs | 82 + .../Commands/ReplaceTemplateItem.cs | 3 + .../Commands/ReplaceTemplateItems.cs | 6 + .../Commands/ReplaceTemplateItemsHandler.cs | 64 + ErsatzTV.Application/Scheduling/Mapper.cs | 56 + .../Scheduling/PlayoutTemplateViewModel.cs | 9 + .../Scheduling/Queries/GetAllBlockGroups.cs | 3 + .../Queries/GetAllBlockGroupsHandler.cs | 21 + .../Queries/GetAllTemplateGroups.cs | 3 + .../Queries/GetAllTemplateGroupsHandler.cs | 23 + .../Scheduling/Queries/GetBlockById.cs | 3 + .../Scheduling/Queries/GetBlockByIdHandler.cs | 17 + .../Scheduling/Queries/GetBlockItems.cs | 3 + .../Queries/GetBlockItemsHandler.cs | 36 + .../Queries/GetBlocksByBlockGroupId.cs | 3 + .../Queries/GetBlocksByBlockGroupIdHandler.cs | 21 + .../Scheduling/Queries/GetPlayoutTemplates.cs | 3 + .../Queries/GetPlayoutTemplatesHandler.cs | 24 + .../Scheduling/Queries/GetTemplateById.cs | 3 + .../Queries/GetTemplateByIdHandler.cs | 17 + .../Scheduling/Queries/GetTemplateItems.cs | 3 + .../Queries/GetTemplateItemsHandler.cs | 19 + .../Queries/GetTemplatesByTemplateGroupId.cs | 3 + .../GetTemplatesByTemplateGroupIdHandler.cs | 21 + .../Scheduling/TemplateGroupViewModel.cs | 3 + .../Scheduling/TemplateItemViewModel.cs | 3 + .../Scheduling/TemplateViewModel.cs | 3 + ErsatzTV.Core/Domain/Playout.cs | 6 +- .../Domain/ProgramSchedulePlayoutType.cs | 1 + ErsatzTV.Core/Domain/Scheduling/Block.cs | 13 + ErsatzTV.Core/Domain/Scheduling/BlockGroup.cs | 8 + ErsatzTV.Core/Domain/Scheduling/BlockItem.cs | 19 + .../Domain/Scheduling/PlayoutHistory.cs | 21 + .../Domain/Scheduling/PlayoutTemplate.cs | 32 + ErsatzTV.Core/Domain/Scheduling/Template.cs | 11 + .../Domain/Scheduling/TemplateGroup.cs | 8 + .../Domain/Scheduling/TemplateItem.cs | 11 + .../Scheduling/IBlockPlayoutBuilder.cs | 9 + .../Scheduling/IExternalJsonPlayoutBuilder.cs | 9 + .../Scheduling/BlockPlayoutBuilder.cs | 367 ++ ErsatzTV.Core/Scheduling/CollectionKey.cs | 41 + .../Scheduling/ExternalJsonPlayoutBuilder.cs | 19 + ErsatzTV.Core/Scheduling/PlayoutBuilder.cs | 7 +- .../Scheduling/PlayoutTemplateSelector.cs | 34 + .../ErsatzTV.Infrastructure.MySql.csproj | 2 +- ...0114034944_Add_BlockScheduling.Designer.cs | 4895 +++++++++++++++++ .../20240114034944_Add_BlockScheduling.cs | 355 ++ .../Migrations/TvContextModelSnapshot.cs | 369 +- .../ErsatzTV.Infrastructure.Sqlite.csproj | 6 +- .../20240110162335_Add_Block.Designer.cs | 4673 ++++++++++++++++ .../Migrations/20240110162335_Add_Block.cs | 153 + .../20240113002246_Add_Template.Designer.cs | 4789 ++++++++++++++++ .../Migrations/20240113002246_Add_Template.cs | 126 + ...0113121929_Add_PlayoutTemplate.Designer.cs | 4837 ++++++++++++++++ .../20240113121929_Add_PlayoutTemplate.cs | 93 + ...dd_PlayoutTemplate_DaysOfMonth.Designer.cs | 4840 ++++++++++++++++ ...3140741_Add_PlayoutTemplate_DaysOfMonth.cs | 28 + ...40113190523_Add_PlayoutHistory.Designer.cs | 4893 ++++++++++++++++ .../20240113190523_Add_PlayoutHistory.cs | 61 + .../Migrations/TvContextModelSnapshot.cs | 393 +- .../Configurations/PlayoutConfiguration.cs | 10 + .../Scheduling/BlockConfiguration.cs | 31 + .../Scheduling/BlockGroupConfiguration.cs | 21 + .../Scheduling/BlockItemConfiguration.cs | 37 + .../Scheduling/PlayoutHistoryConfiguration.cs | 13 + .../PlayoutTemplateConfiguration.cs | 22 + .../Scheduling/TemplateConfiguration.cs | 26 + .../Scheduling/TemplateGroupConfiguration.cs | 21 + .../Scheduling/TemplateItemConfiguration.cs | 13 + ErsatzTV.Infrastructure/Data/TvContext.cs | 8 + .../ErsatzTV.Infrastructure.csproj | 12 +- ErsatzTV/ErsatzTV.csproj | 13 +- ErsatzTV/Pages/BlockEditor.razor | 375 ++ ErsatzTV/Pages/Blocks.razor | 197 + ErsatzTV/Pages/PlayoutEditor.razor | 43 +- ErsatzTV/Pages/PlayoutTemplatesEditor.razor | 555 ++ ErsatzTV/Pages/Playouts.razor | 51 +- ErsatzTV/Pages/TemplateEditor.razor | 254 + ErsatzTV/Pages/Templates.razor | 191 + ErsatzTV/PlayoutKind.cs | 7 + ErsatzTV/Services/ScannerService.cs | 2 +- ErsatzTV/Services/SchedulerService.cs | 2 + ErsatzTV/Shared/MainLayout.razor | 8 +- ErsatzTV/Startup.cs | 2 + .../PlayoutEditViewModelValidator.cs | 4 +- ErsatzTV/ViewModels/BlockItemEditViewModel.cs | 75 + .../ViewModels/BlockItemsEditViewModel.cs | 8 + ErsatzTV/ViewModels/BlockTreeItemViewModel.cs | 41 + ErsatzTV/ViewModels/PlayoutEditViewModel.cs | 10 +- .../PlayoutTemplateEditViewModel.cs | 18 + .../ViewModels/TemplateItemEditViewModel.cs | 22 + .../ViewModels/TemplateItemsEditViewModel.cs | 7 + .../ViewModels/TemplateTreeItemViewModel.cs | 37 + ErsatzTV/_Imports.razor | 1 + 126 files changed, 34235 insertions(+), 78 deletions(-) create mode 100644 ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs create mode 100644 ErsatzTV.Application/Scheduling/BlockItemViewModel.cs create mode 100644 ErsatzTV.Application/Scheduling/BlockViewModel.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateBlock.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateBlockGroup.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateTemplate.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroup.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteBlock.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroup.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroupHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteBlockHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteTemplate.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroup.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroupHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/DeleteTemplateHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItem.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItems.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplate.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItems.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItemsHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItem.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItems.cs create mode 100644 ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItemsHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Mapper.cs create mode 100644 ErsatzTV.Application/Scheduling/PlayoutTemplateViewModel.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroups.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetAllTemplateGroups.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetAllTemplateGroupsHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetBlockById.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetBlockByIdHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetBlockItems.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetBlockItemsHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetBlocksByBlockGroupId.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetBlocksByBlockGroupIdHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplates.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplatesHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplateById.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplateByIdHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplateItems.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplateItemsHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplatesByTemplateGroupId.cs create mode 100644 ErsatzTV.Application/Scheduling/Queries/GetTemplatesByTemplateGroupIdHandler.cs create mode 100644 ErsatzTV.Application/Scheduling/TemplateGroupViewModel.cs create mode 100644 ErsatzTV.Application/Scheduling/TemplateItemViewModel.cs create mode 100644 ErsatzTV.Application/Scheduling/TemplateViewModel.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/Block.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/BlockGroup.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/BlockItem.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/PlayoutTemplate.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/Template.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/TemplateGroup.cs create mode 100644 ErsatzTV.Core/Domain/Scheduling/TemplateItem.cs create mode 100644 ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs create mode 100644 ErsatzTV.Core/Interfaces/Scheduling/IExternalJsonPlayoutBuilder.cs create mode 100644 ErsatzTV.Core/Scheduling/BlockPlayoutBuilder.cs create mode 100644 ErsatzTV.Core/Scheduling/ExternalJsonPlayoutBuilder.cs create mode 100644 ErsatzTV.Core/Scheduling/PlayoutTemplateSelector.cs create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20240114034944_Add_BlockScheduling.Designer.cs create mode 100644 ErsatzTV.Infrastructure.MySql/Migrations/20240114034944_Add_BlockScheduling.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240110162335_Add_Block.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240110162335_Add_Block.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113002246_Add_Template.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113002246_Add_Template.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113121929_Add_PlayoutTemplate.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113121929_Add_PlayoutTemplate.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113140741_Add_PlayoutTemplate_DaysOfMonth.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113140741_Add_PlayoutTemplate_DaysOfMonth.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113190523_Add_PlayoutHistory.Designer.cs create mode 100644 ErsatzTV.Infrastructure.Sqlite/Migrations/20240113190523_Add_PlayoutHistory.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockGroupConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockItemConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/PlayoutHistoryConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/PlayoutTemplateConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateGroupConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateItemConfiguration.cs create mode 100644 ErsatzTV/Pages/BlockEditor.razor create mode 100644 ErsatzTV/Pages/Blocks.razor create mode 100644 ErsatzTV/Pages/PlayoutTemplatesEditor.razor create mode 100644 ErsatzTV/Pages/TemplateEditor.razor create mode 100644 ErsatzTV/Pages/Templates.razor create mode 100644 ErsatzTV/PlayoutKind.cs create mode 100644 ErsatzTV/ViewModels/BlockItemEditViewModel.cs create mode 100644 ErsatzTV/ViewModels/BlockItemsEditViewModel.cs create mode 100644 ErsatzTV/ViewModels/BlockTreeItemViewModel.cs create mode 100644 ErsatzTV/ViewModels/PlayoutTemplateEditViewModel.cs create mode 100644 ErsatzTV/ViewModels/TemplateItemEditViewModel.cs create mode 100644 ErsatzTV/ViewModels/TemplateItemsEditViewModel.cs create mode 100644 ErsatzTV/ViewModels/TemplateTreeItemViewModel.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index ec747a43..7d27a801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - If found, ErsatzTV will run ffprobe to get statistics immediately before streaming from disk - When local files are unavailable, ErsatzTV must be logged into the same Plex server as DizqueTV - ErsatzTV will ask Plex for statistics immediately before streaming from Plex +- Add new *experimental* playout type `Block` + - **This playout type is under active development and updates may reset or delete related playout data** + - Many planned features are missing, incomplete, or result in errors. This is expected. + - Block playouts consist of: + - `Blocks` - ordered list of items to play within the specified duration + - `Templates` - a generic "day" that consists of blocks scheduled at specific times + - `Playout Templates` - templates to schedule using the specified criteria. Only one template will be selected each day + - Much more to come on this feature as development continues ### Fixed - Fix error loading path replacements when using MySql diff --git a/Directory.Build.props b/Directory.Build.props index c4d1aa6e..649687ae 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@ develop + false \ No newline at end of file diff --git a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings index 0dc00038..8df6a2bf 100644 --- a/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings +++ b/ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings @@ -35,6 +35,8 @@ True True True + True + True True True True diff --git a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs index 50049074..42b40cc8 100644 --- a/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs @@ -22,12 +22,16 @@ public class BuildPlayoutHandler : IRequestHandler _workerChannel; public BuildPlayoutHandler( IClient client, IDbContextFactory dbContextFactory, IPlayoutBuilder playoutBuilder, + IBlockPlayoutBuilder blockPlayoutBuilder, + IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder, IFFmpegSegmenterService ffmpegSegmenterService, IEntityLocker entityLocker, ChannelWriter workerChannel) @@ -35,6 +39,8 @@ public class BuildPlayoutHandler : IRequestHandler p.Channel) .Include(p => p.Items) + .Include(p => p.PlayoutHistory) + .Include(p => p.Templates) + .ThenInclude(t => t.Template) + .ThenInclude(t => t.Items) + .ThenInclude(i => i.Block) + .ThenInclude(b => b.Items) .Include(p => p.FillGroupIndices) .ThenInclude(fgi => fgi.EnumeratorState) .Include(p => p.ProgramScheduleAlternates) diff --git a/ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs new file mode 100644 index 00000000..fe8cf965 --- /dev/null +++ b/ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs @@ -0,0 +1,67 @@ +using System.Threading.Channels; +using ErsatzTV.Application.Channels; +using ErsatzTV.Core; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; +using Channel = ErsatzTV.Core.Domain.Channel; + +namespace ErsatzTV.Application.Playouts; + +public class CreateBlockPlayoutHandler( + ChannelWriter channel, + IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + CreateBlockPlayout request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(dbContext, request); + return await validation.Apply(playout => PersistPlayout(dbContext, playout)); + } + + private async Task PersistPlayout(TvContext dbContext, Playout playout) + { + await dbContext.Playouts.AddAsync(playout); + await dbContext.SaveChangesAsync(); + await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); + await channel.WriteAsync(new RefreshChannelList()); + return new CreatePlayoutResponse(playout.Id); + } + + private static async Task> Validate( + TvContext dbContext, + CreateBlockPlayout request) => + (await ValidateChannel(dbContext, request), ValidatePlayoutType(request)) + .Apply( + (channel, playoutType) => new Playout + { + ChannelId = channel.Id, + ProgramSchedulePlayoutType = playoutType + }); + + private static Task> ValidateChannel( + TvContext dbContext, + CreateBlockPlayout createBlockPlayout) => + dbContext.Channels + .Include(c => c.Playouts) + .SelectOneAsync(c => c.Id, c => c.Id == createBlockPlayout.ChannelId) + .Map(o => o.ToValidation("Channel does not exist")) + .BindT(ChannelMustNotHavePlayouts); + + private static Validation ChannelMustNotHavePlayouts(Channel channel) => + Optional(channel.Playouts.Count) + .Filter(count => count == 0) + .Map(_ => channel) + .ToValidation("Channel already has one playout"); + + private static Validation ValidatePlayoutType( + CreateBlockPlayout createBlockPlayout) => + Optional(createBlockPlayout.ProgramSchedulePlayoutType) + .Filter(playoutType => playoutType == ProgramSchedulePlayoutType.Block) + .ToValidation("[ProgramSchedulePlayoutType] must be Block"); +} diff --git a/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs b/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs index 3ae417a7..bca24a60 100644 --- a/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs +++ b/ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs @@ -9,5 +9,8 @@ public record CreatePlayout(int ChannelId, ProgramSchedulePlayoutType ProgramSch public record CreateFloodPlayout(int ChannelId, int ProgramScheduleId) : CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Flood); +public record CreateBlockPlayout(int ChannelId) + : CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Block); + public record CreateExternalJsonPlayout(int ChannelId, string ExternalJsonFile) : CreatePlayout(ChannelId, ProgramSchedulePlayoutType.ExternalJson); diff --git a/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs index f0e1345e..7cf5ae46 100644 --- a/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs @@ -25,9 +25,7 @@ public class DeletePlayoutHandler : IRequestHandler> Handle( - DeletePlayout request, - CancellationToken cancellationToken) + public async Task> Handle(DeletePlayout request, CancellationToken cancellationToken) { await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs index 7f010ddf..3921f2b5 100644 --- a/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs +++ b/ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs @@ -56,7 +56,7 @@ public class GetProgramScheduleItemsHandler : .Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList()); } - // shuffled schedule items supports a limited set of properly values + // shuffled schedule items supports a limited set of property values private static ProgramScheduleItemViewModel EnforceProperties( Option maybeProgramSchedule, ProgramScheduleItemViewModel item) diff --git a/ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs b/ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs new file mode 100644 index 00000000..89d8e3a5 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record BlockGroupViewModel(int Id, string Name, int BlockCount); diff --git a/ErsatzTV.Application/Scheduling/BlockItemViewModel.cs b/ErsatzTV.Application/Scheduling/BlockItemViewModel.cs new file mode 100644 index 00000000..5fb3b6c8 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/BlockItemViewModel.cs @@ -0,0 +1,15 @@ +using ErsatzTV.Application.MediaCollections; +using ErsatzTV.Application.MediaItems; +using ErsatzTV.Core.Domain; + +namespace ErsatzTV.Application.Scheduling; + +public record BlockItemViewModel( + int Id, + int Index, + ProgramScheduleItemCollectionType CollectionType, + MediaCollectionViewModel Collection, + MultiCollectionViewModel MultiCollection, + SmartCollectionViewModel SmartCollection, + NamedMediaItemViewModel MediaItem, + PlaybackOrder PlaybackOrder); diff --git a/ErsatzTV.Application/Scheduling/BlockViewModel.cs b/ErsatzTV.Application/Scheduling/BlockViewModel.cs new file mode 100644 index 00000000..d0d3023b --- /dev/null +++ b/ErsatzTV.Application/Scheduling/BlockViewModel.cs @@ -0,0 +1,3 @@ +namespace ErsatzTV.Application.Scheduling; + +public record BlockViewModel(int Id, string Name, int Minutes); diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateBlock.cs b/ErsatzTV.Application/Scheduling/Commands/CreateBlock.cs new file mode 100644 index 00000000..ea56e921 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateBlock.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record CreateBlock(int BlockGroupId, string Name) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroup.cs b/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroup.cs new file mode 100644 index 00000000..06d7fa07 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroup.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record CreateBlockGroup(string Name) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs new file mode 100644 index 00000000..13e46693 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs @@ -0,0 +1,31 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class CreateBlockGroupHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle(CreateBlockGroup request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(request); + return await validation.Apply(profile => PersistBlockGroup(dbContext, profile)); + } + + private static async Task PersistBlockGroup(TvContext dbContext, BlockGroup blockGroup) + { + await dbContext.BlockGroups.AddAsync(blockGroup); + await dbContext.SaveChangesAsync(); + return Mapper.ProjectToViewModel(blockGroup); + } + + private static Task> Validate(CreateBlockGroup request) => + Task.FromResult(ValidateName(request).Map(name => new BlockGroup { Name = name, Blocks = [] })); + + private static Validation ValidateName(CreateBlockGroup createBlockGroup) => + createBlockGroup.NotEmpty(x => x.Name) + .Bind(_ => createBlockGroup.NotLongerThan(50)(x => x.Name)); +} diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs new file mode 100644 index 00000000..2c515516 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs @@ -0,0 +1,40 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class CreateBlockHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + CreateBlock request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(request); + return await validation.Apply(profile => PersistBlock(dbContext, profile)); + } + + private static async Task PersistBlock(TvContext dbContext, Block block) + { + await dbContext.Blocks.AddAsync(block); + await dbContext.SaveChangesAsync(); + return Mapper.ProjectToViewModel(block); + } + + private static Task> Validate(CreateBlock request) => + Task.FromResult( + ValidateName(request).Map( + name => new Block + { + BlockGroupId = request.BlockGroupId, + Name = name, + Minutes = 30 + })); + + private static Validation ValidateName(CreateBlock createBlock) => + createBlock.NotEmpty(x => x.Name) + .Bind(_ => createBlock.NotLongerThan(50)(x => x.Name)); +} diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateTemplate.cs b/ErsatzTV.Application/Scheduling/Commands/CreateTemplate.cs new file mode 100644 index 00000000..c7f5c0df --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateTemplate.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record CreateTemplate(int TemplateGroupId, string Name) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroup.cs b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroup.cs new file mode 100644 index 00000000..03d0cc44 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroup.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record CreateTemplateGroup(string Name) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs new file mode 100644 index 00000000..d817b983 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs @@ -0,0 +1,35 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class CreateTemplateGroupHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + CreateTemplateGroup request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(request); + return await validation.Apply(profile => PersistTemplateGroup(dbContext, profile)); + } + + private static async Task PersistTemplateGroup( + TvContext dbContext, + TemplateGroup templateGroup) + { + await dbContext.TemplateGroups.AddAsync(templateGroup); + await dbContext.SaveChangesAsync(); + return Mapper.ProjectToViewModel(templateGroup); + } + + private static Task> Validate(CreateTemplateGroup request) => + Task.FromResult(ValidateName(request).Map(name => new TemplateGroup { Name = name, Templates = [] })); + + private static Validation ValidateName(CreateTemplateGroup createTemplateGroup) => + createTemplateGroup.NotEmpty(x => x.Name) + .Bind(_ => createTemplateGroup.NotLongerThan(50)(x => x.Name)); +} diff --git a/ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs new file mode 100644 index 00000000..33f708d3 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs @@ -0,0 +1,39 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class CreateTemplateHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle( + CreateTemplate request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + Validation validation = await Validate(request); + return await validation.Apply(profile => PersistTemplate(dbContext, profile)); + } + + private static async Task PersistTemplate(TvContext dbContext, Template template) + { + await dbContext.Templates.AddAsync(template); + await dbContext.SaveChangesAsync(); + return Mapper.ProjectToViewModel(template); + } + + private static Task> Validate(CreateTemplate request) => + Task.FromResult( + ValidateName(request).Map( + name => new Template + { + TemplateGroupId = request.TemplateGroupId, + Name = name + })); + + private static Validation ValidateName(CreateTemplate createTemplate) => + createTemplate.NotEmpty(x => x.Name) + .Bind(_ => createTemplate.NotLongerThan(50)(x => x.Name)); +} diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteBlock.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteBlock.cs new file mode 100644 index 00000000..190ebd81 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteBlock.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record DeleteBlock(int BlockId) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroup.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroup.cs new file mode 100644 index 00000000..69efa19d --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroup.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record DeleteBlockGroup(int BlockGroupId) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroupHandler.cs new file mode 100644 index 00000000..28949cdf --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroupHandler.cs @@ -0,0 +1,29 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class DeleteBlockGroupHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle(DeleteBlockGroup request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybeBlockGroup = await dbContext.BlockGroups + .SelectOneAsync(p => p.Id, p => p.Id == request.BlockGroupId); + + foreach (BlockGroup blockGroup in maybeBlockGroup) + { + dbContext.BlockGroups.Remove(blockGroup); + await dbContext.SaveChangesAsync(cancellationToken); + } + + return maybeBlockGroup.Match( + _ => Option.None, + () => BaseError.New($"BlockGroup {request.BlockGroupId} does not exist.")); + } +} diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteBlockHandler.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteBlockHandler.cs new file mode 100644 index 00000000..e8417305 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteBlockHandler.cs @@ -0,0 +1,29 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class DeleteBlockHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle(DeleteBlock request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybeBlock = await dbContext.Blocks + .SelectOneAsync(p => p.Id, p => p.Id == request.BlockId); + + foreach (Block block in maybeBlock) + { + dbContext.Blocks.Remove(block); + await dbContext.SaveChangesAsync(cancellationToken); + } + + return maybeBlock.Match( + _ => Option.None, + () => BaseError.New($"Block {request.BlockId} does not exist.")); + } +} diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteTemplate.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplate.cs new file mode 100644 index 00000000..52695dce --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplate.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record DeleteTemplate(int TemplateId) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroup.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroup.cs new file mode 100644 index 00000000..f3c06c23 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroup.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core; + +namespace ErsatzTV.Application.Scheduling; + +public record DeleteTemplateGroup(int TemplateGroupId) : IRequest>; diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroupHandler.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroupHandler.cs new file mode 100644 index 00000000..cd99139a --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroupHandler.cs @@ -0,0 +1,29 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class DeleteTemplateGroupHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle(DeleteTemplateGroup request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option maybeTemplateGroup = await dbContext.TemplateGroups + .SelectOneAsync(p => p.Id, p => p.Id == request.TemplateGroupId); + + foreach (TemplateGroup templateGroup in maybeTemplateGroup) + { + dbContext.TemplateGroups.Remove(templateGroup); + await dbContext.SaveChangesAsync(cancellationToken); + } + + return maybeTemplateGroup.Match( + _ => Option.None, + () => BaseError.New($"TemplateGroup {request.TemplateGroupId} does not exist.")); + } +} diff --git a/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateHandler.cs b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateHandler.cs new file mode 100644 index 00000000..5cdee218 --- /dev/null +++ b/ErsatzTV.Application/Scheduling/Commands/DeleteTemplateHandler.cs @@ -0,0 +1,29 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Domain.Scheduling; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using Microsoft.EntityFrameworkCore; + +namespace ErsatzTV.Application.Scheduling; + +public class DeleteTemplateHandler(IDbContextFactory dbContextFactory) + : IRequestHandler> +{ + public async Task> Handle(DeleteTemplate request, CancellationToken cancellationToken) + { + await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); + + Option