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