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