Browse Source

add deco system (#1665)

* add deco groups and decos; set default deco for block playout

* use block playout default deco for watermark

* add deco templates, groups and deco template editor

* associate deco template with playout template

* use deco template item watermark for playback

* update changelog for decos
pull/1666/head
Jason Dove 2 years ago committed by GitHub
parent
commit
19fc068a73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      CHANGELOG.md
  2. 5
      ErsatzTV.Application/Scheduling/Commands/CreateDeco.cs
  3. 5
      ErsatzTV.Application/Scheduling/Commands/CreateDecoGroup.cs
  4. 33
      ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs
  5. 52
      ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs
  6. 5
      ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplate.cs
  7. 5
      ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroup.cs
  8. 35
      ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs
  9. 39
      ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs
  10. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteDeco.cs
  11. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteDecoGroup.cs
  12. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteDecoGroupHandler.cs
  13. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteDecoHandler.cs
  14. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplate.cs
  15. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplateGroup.cs
  16. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplateGroupHandler.cs
  17. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplateHandler.cs
  18. 3
      ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItem.cs
  19. 6
      ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItems.cs
  20. 66
      ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItemsHandler.cs
  21. 1
      ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplate.cs
  22. 2
      ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItemsHandler.cs
  23. 5
      ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs
  24. 61
      ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs
  25. 3
      ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDeco.cs
  26. 16
      ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDecoHandler.cs
  27. 3
      ErsatzTV.Application/Scheduling/DecoGroupViewModel.cs
  28. 3
      ErsatzTV.Application/Scheduling/DecoTemplateGroupViewModel.cs
  29. 3
      ErsatzTV.Application/Scheduling/DecoTemplateItemViewModel.cs
  30. 3
      ErsatzTV.Application/Scheduling/DecoTemplateViewModel.cs
  31. 3
      ErsatzTV.Application/Scheduling/DecoViewModel.cs
  32. 27
      ErsatzTV.Application/Scheduling/Mapper.cs
  33. 1
      ErsatzTV.Application/Scheduling/PlayoutTemplateViewModel.cs
  34. 3
      ErsatzTV.Application/Scheduling/Queries/GetAllDecoGroups.cs
  35. 21
      ErsatzTV.Application/Scheduling/Queries/GetAllDecoGroupsHandler.cs
  36. 3
      ErsatzTV.Application/Scheduling/Queries/GetAllDecoTemplateGroups.cs
  37. 23
      ErsatzTV.Application/Scheduling/Queries/GetAllDecoTemplateGroupsHandler.cs
  38. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoById.cs
  39. 17
      ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs
  40. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutId.cs
  41. 18
      ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs
  42. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateById.cs
  43. 17
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateByIdHandler.cs
  44. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateItems.cs
  45. 20
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateItemsHandler.cs
  46. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplatesByDecoTemplateGroupId.cs
  47. 21
      ErsatzTV.Application/Scheduling/Queries/GetDecoTemplatesByDecoTemplateGroupIdHandler.cs
  48. 3
      ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupId.cs
  49. 21
      ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupIdHandler.cs
  50. 1
      ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplatesHandler.cs
  51. 66
      ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs
  52. 2
      ErsatzTV.Core/Domain/Playout.cs
  53. 16
      ErsatzTV.Core/Domain/Scheduling/Deco.cs
  54. 8
      ErsatzTV.Core/Domain/Scheduling/DecoGroup.cs
  55. 12
      ErsatzTV.Core/Domain/Scheduling/DecoTemplate.cs
  56. 8
      ErsatzTV.Core/Domain/Scheduling/DecoTemplateGroup.cs
  57. 12
      ErsatzTV.Core/Domain/Scheduling/DecoTemplateItem.cs
  58. 2
      ErsatzTV.Core/Domain/Scheduling/PlayoutTemplate.cs
  59. 5404
      ErsatzTV.Infrastructure.MySql/Migrations/20240404160439_Add_Deco_Group_Deco.Designer.cs
  60. 117
      ErsatzTV.Infrastructure.MySql/Migrations/20240404160439_Add_Deco_Group_Deco.cs
  61. 5533
      ErsatzTV.Infrastructure.MySql/Migrations/20240404182555_Add_DecoTemplate.Designer.cs
  62. 154
      ErsatzTV.Infrastructure.MySql/Migrations/20240404182555_Add_DecoTemplate.cs
  63. 215
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  64. 5255
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240403230520_Add_Deco_Group_Deco.Designer.cs
  65. 112
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240403230520_Add_Deco_Group_Deco.cs
  66. 5375
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240404180108_Add_DecoTemplate.Designer.cs
  67. 147
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240404180108_Add_DecoTemplate.cs
  68. 5378
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240404182159_Add_DecoTemplateItem_EndTime.Designer.cs
  69. 30
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240404182159_Add_DecoTemplateItem_EndTime.cs
  70. 205
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  71. 28
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoConfiguration.cs
  72. 21
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoGroupConfiguration.cs
  73. 27
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateConfiguration.cs
  74. 21
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateGroupConfiguration.cs
  75. 10
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateItemConfiguration.cs
  76. 17
      ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs
  77. 5
      ErsatzTV.Infrastructure/Data/TvContext.cs
  78. 157
      ErsatzTV/Pages/BlockPlayoutEditor.razor
  79. 90
      ErsatzTV/Pages/DecoEditor.razor
  80. 314
      ErsatzTV/Pages/DecoTemplateEditor.razor
  81. 205
      ErsatzTV/Pages/DecoTemplates.razor
  82. 211
      ErsatzTV/Pages/Decos.razor
  83. 42
      ErsatzTV/Pages/PlayoutTemplatesEditor.razor
  84. 20
      ErsatzTV/Pages/Playouts.razor
  85. 10
      ErsatzTV/Shared/MainLayout.razor
  86. 8
      ErsatzTV/ViewModels/DecoEditViewModel.cs
  87. 22
      ErsatzTV/ViewModels/DecoTemplateItemEditViewModel.cs
  88. 7
      ErsatzTV/ViewModels/DecoTemplateItemsEditViewModel.cs
  89. 37
      ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs
  90. 40
      ErsatzTV/ViewModels/DecoTreeItemViewModel.cs
  91. 3
      ErsatzTV/ViewModels/PlayoutTemplateEditViewModel.cs

9
CHANGELOG.md

@ -8,6 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -8,6 +8,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add `Active Date Range` to block playout template editor to allow limiting templates to a specific date range
- This is year-agnostic, meaning the Month/Day range will apply to every year
- This also supports wrapping the end of the year (e.g., start 12/1 and end 1/15)
- Add new `Deco` system for "decorating" channels with non-primary content
- Decos currently only contain Watermarks, but future work will add other functionality, including filler
- Similar to blocks, decos have deco groups for organization
- Similar to blocks, decos have deco templates for filling a "day" with decos
- In the playout template editor, playout template items can have *both* a block template and a deco template
- This allows watermarks to change at different times than primary content
- Block playouts can also have a default deco
- This will apply whenever a deco template is missing, or when a deco template item cannot be found for the current time
- Effectively, this sets a default watermark for the entire playout
## [0.8.6-beta] - 2024-04-03
### Added

5
ErsatzTV.Application/Scheduling/Commands/CreateDeco.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record CreateDeco(int DecoGroupId, string Name) : IRequest<Either<BaseError, DecoViewModel>>;

5
ErsatzTV.Application/Scheduling/Commands/CreateDecoGroup.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record CreateDecoGroup(string Name) : IRequest<Either<BaseError, DecoGroupViewModel>>;

33
ErsatzTV.Application/Scheduling/Commands/CreateDecoGroupHandler.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class CreateDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateDecoGroup, Either<BaseError, DecoGroupViewModel>>
{
public async Task<Either<BaseError, DecoGroupViewModel>> Handle(
CreateDecoGroup request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoGroup> validation = await Validate(request);
return await validation.Apply(profile => PersistDecoGroup(dbContext, profile));
}
private static async Task<DecoGroupViewModel> PersistDecoGroup(TvContext dbContext, DecoGroup decoGroup)
{
await dbContext.DecoGroups.AddAsync(decoGroup);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(decoGroup);
}
private static Task<Validation<BaseError, DecoGroup>> Validate(CreateDecoGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new DecoGroup { Name = name, Decos = [] }));
private static Validation<BaseError, string> ValidateName(CreateDecoGroup createDecoGroup) =>
createDecoGroup.NotEmpty(x => x.Name)
.Bind(_ => createDecoGroup.NotLongerThan(50)(x => x.Name));
}

52
ErsatzTV.Application/Scheduling/Commands/CreateDecoHandler.cs

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class CreateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateDeco, Either<BaseError, DecoViewModel>>
{
public async Task<Either<BaseError, DecoViewModel>> Handle(
CreateDeco request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Deco> validation = await Validate(dbContext, request);
return await validation.Apply(profile => PersistDeco(dbContext, profile));
}
private static async Task<DecoViewModel> PersistDeco(TvContext dbContext, Deco deco)
{
await dbContext.Decos.AddAsync(deco);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(deco);
}
private static async Task<Validation<BaseError, Deco>> Validate(TvContext dbContext, CreateDeco request) =>
await ValidateDecoName(dbContext, request).MapT(
name => new Deco
{
DecoGroupId = request.DecoGroupId,
Name = name
});
private static async Task<Validation<BaseError, string>> ValidateDecoName(
TvContext dbContext,
CreateDeco request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Deco name \"{request.Name}\" is invalid");
}
Option<Deco> maybeExisting = await dbContext.Decos
.FirstOrDefaultAsync(r => r.DecoGroupId == request.DecoGroupId && r.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A deco named \"{request.Name}\" already exists in that deco group")
: Success<BaseError, string>(request.Name);
}
}

5
ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplate.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record CreateDecoTemplate(int DecoTemplateGroupId, string Name) : IRequest<Either<BaseError, DecoTemplateViewModel>>;

5
ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroup.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record CreateDecoTemplateGroup(string Name) : IRequest<Either<BaseError, DecoTemplateGroupViewModel>>;

35
ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateGroupHandler.cs

@ -0,0 +1,35 @@ @@ -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 CreateDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateDecoTemplateGroup, Either<BaseError, DecoTemplateGroupViewModel>>
{
public async Task<Either<BaseError, DecoTemplateGroupViewModel>> Handle(
CreateDecoTemplateGroup request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoTemplateGroup> validation = await Validate(request);
return await validation.Apply(profile => PersistDecoTemplateGroup(dbContext, profile));
}
private static async Task<DecoTemplateGroupViewModel> PersistDecoTemplateGroup(
TvContext dbContext,
DecoTemplateGroup decoDecoTemplateGroup)
{
await dbContext.DecoTemplateGroups.AddAsync(decoDecoTemplateGroup);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(decoDecoTemplateGroup);
}
private static Task<Validation<BaseError, DecoTemplateGroup>> Validate(CreateDecoTemplateGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new DecoTemplateGroup { Name = name, DecoTemplates = [] }));
private static Validation<BaseError, string> ValidateName(CreateDecoTemplateGroup createDecoTemplateGroup) =>
createDecoTemplateGroup.NotEmpty(x => x.Name)
.Bind(_ => createDecoTemplateGroup.NotLongerThan(50)(x => x.Name));
}

39
ErsatzTV.Application/Scheduling/Commands/CreateDecoTemplateHandler.cs

@ -0,0 +1,39 @@ @@ -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 CreateDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateDecoTemplate, Either<BaseError, DecoTemplateViewModel>>
{
public async Task<Either<BaseError, DecoTemplateViewModel>> Handle(
CreateDecoTemplate request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoTemplate> validation = await Validate(request);
return await validation.Apply(profile => PersistDecoTemplate(dbContext, profile));
}
private static async Task<DecoTemplateViewModel> PersistDecoTemplate(TvContext dbContext, DecoTemplate decoTemplate)
{
await dbContext.DecoTemplates.AddAsync(decoTemplate);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(decoTemplate);
}
private static Task<Validation<BaseError, DecoTemplate>> Validate(CreateDecoTemplate request) =>
Task.FromResult(
ValidateName(request).Map(
name => new DecoTemplate
{
DecoTemplateGroupId = request.DecoTemplateGroupId,
Name = name
}));
private static Validation<BaseError, string> ValidateName(CreateDecoTemplate createDecoTemplate) =>
createDecoTemplate.NotEmpty(x => x.Name)
.Bind(_ => createDecoTemplate.NotLongerThan(50)(x => x.Name));
}

5
ErsatzTV.Application/Scheduling/Commands/DeleteDeco.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record DeleteDeco(int DecoId) : IRequest<Option<BaseError>>;

5
ErsatzTV.Application/Scheduling/Commands/DeleteDecoGroup.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record DeleteDecoGroup(int DecoGroupId) : IRequest<Option<BaseError>>;

29
ErsatzTV.Application/Scheduling/Commands/DeleteDecoGroupHandler.cs

@ -0,0 +1,29 @@ @@ -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 DeleteDecoGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteDecoGroup, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteDecoGroup request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<DecoGroup> maybeDecoGroup = await dbContext.DecoGroups
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoGroupId);
foreach (DecoGroup decoGroup in maybeDecoGroup)
{
dbContext.DecoGroups.Remove(decoGroup);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeDecoGroup.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"DecoGroup {request.DecoGroupId} does not exist."));
}
}

29
ErsatzTV.Application/Scheduling/Commands/DeleteDecoHandler.cs

@ -0,0 +1,29 @@ @@ -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 DeleteDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteDeco, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteDeco request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Deco> maybeDeco = await dbContext.Decos
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoId);
foreach (Deco deco in maybeDeco)
{
dbContext.Decos.Remove(deco);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeDeco.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"Deco {request.DecoId} does not exist."));
}
}

5
ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplate.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record DeleteDecoTemplate(int DecoTemplateId) : IRequest<Option<BaseError>>;

5
ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplateGroup.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record DeleteDecoTemplateGroup(int DecoTemplateGroupId) : IRequest<Option<BaseError>>;

29
ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplateGroupHandler.cs

@ -0,0 +1,29 @@ @@ -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 DeleteDecoTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteDecoTemplateGroup, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteDecoTemplateGroup request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<DecoTemplateGroup> maybeDecoTemplateGroup = await dbContext.DecoTemplateGroups
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoTemplateGroupId);
foreach (DecoTemplateGroup decoTemplateGroup in maybeDecoTemplateGroup)
{
dbContext.DecoTemplateGroups.Remove(decoTemplateGroup);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeDecoTemplateGroup.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"DecoTemplateGroup {request.DecoTemplateGroupId} does not exist."));
}
}

29
ErsatzTV.Application/Scheduling/Commands/DeleteDecoTemplateHandler.cs

@ -0,0 +1,29 @@ @@ -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 DeleteDecoTemplateHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteDecoTemplate, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteDecoTemplate request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<DecoTemplate> maybeDecoTemplate = await dbContext.DecoTemplates
.SelectOneAsync(p => p.Id, p => p.Id == request.DecoTemplateId);
foreach (DecoTemplate decoTemplate in maybeDecoTemplate)
{
dbContext.DecoTemplates.Remove(decoTemplate);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeDecoTemplate.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"DecoTemplate {request.DecoTemplateId} does not exist."));
}
}

3
ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItem.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record ReplaceDecoTemplateItem(int DecoId, TimeSpan StartTime, TimeSpan EndTime);

6
ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItems.cs

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record ReplaceDecoTemplateItems(int DecoTemplateId, string Name, List<ReplaceDecoTemplateItem> Items)
: IRequest<Either<BaseError, List<DecoTemplateItemViewModel>>>;

66
ErsatzTV.Application/Scheduling/Commands/ReplaceDecoTemplateItemsHandler.cs

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
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 ReplaceDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<ReplaceDecoTemplateItems, Either<BaseError, List<DecoTemplateItemViewModel>>>
{
public async Task<Either<BaseError, List<DecoTemplateItemViewModel>>> Handle(
ReplaceDecoTemplateItems request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, DecoTemplate> validation = await Validate(dbContext, request);
return await validation.Apply(ps => Persist(dbContext, request, ps));
}
private static async Task<List<DecoTemplateItemViewModel>> Persist(
TvContext dbContext,
ReplaceDecoTemplateItems request,
DecoTemplate decoTemplate)
{
decoTemplate.Name = request.Name;
decoTemplate.DateUpdated = DateTime.UtcNow;
dbContext.RemoveRange(decoTemplate.Items);
decoTemplate.Items = request.Items.Map(i => BuildItem(decoTemplate, i)).ToList();
await dbContext.SaveChangesAsync();
// TODO: refresh any playouts that use this schedule
// foreach (Playout playout in programSchedule.Playouts)
// {
// await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh));
// }
await dbContext.Entry(decoTemplate)
.Collection(t => t.Items)
.Query()
.Include(i => i.Deco)
.LoadAsync();
return decoTemplate.Items.Map(Mapper.ProjectToViewModel).ToList();
}
private static DecoTemplateItem BuildItem(DecoTemplate decoTemplate, ReplaceDecoTemplateItem item) =>
new()
{
DecoTemplateId = decoTemplate.Id,
DecoId = item.DecoId,
StartTime = item.StartTime,
EndTime = item.EndTime
};
private static Task<Validation<BaseError, DecoTemplate>> Validate(TvContext dbContext, ReplaceDecoTemplateItems request) =>
DecoTemplateMustExist(dbContext, request.DecoTemplateId);
private static Task<Validation<BaseError, DecoTemplate>> DecoTemplateMustExist(TvContext dbContext, int decoTemplateId) =>
dbContext.DecoTemplates
.Include(b => b.Items)
.SelectOneAsync(b => b.Id, b => b.Id == decoTemplateId)
.Map(o => o.ToValidation<BaseError>("[DecoTemplateId] does not exist."));
}

1
ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplate.cs

@ -4,6 +4,7 @@ public record ReplacePlayoutTemplate( @@ -4,6 +4,7 @@ public record ReplacePlayoutTemplate(
int Id,
int Index,
int TemplateId,
int? DecoTemplateId,
List<DayOfWeek> DaysOfWeek,
List<int> DaysOfMonth,
List<int> MonthsOfYear,

2
ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItemsHandler.cs

@ -52,6 +52,7 @@ public class ReplacePlayoutTemplateItemsHandler( @@ -52,6 +52,7 @@ public class ReplacePlayoutTemplateItemsHandler(
PlayoutId = playout.Id,
Index = add.Index,
TemplateId = add.TemplateId,
DecoTemplateId = add.DecoTemplateId,
DaysOfWeek = add.DaysOfWeek,
DaysOfMonth = add.DaysOfMonth,
MonthsOfYear = add.MonthsOfYear,
@ -70,6 +71,7 @@ public class ReplacePlayoutTemplateItemsHandler( @@ -70,6 +71,7 @@ public class ReplacePlayoutTemplateItemsHandler(
{
ex.Index = update.Index;
ex.TemplateId = update.TemplateId;
ex.DecoTemplateId = update.DecoTemplateId;
ex.DaysOfWeek = update.DaysOfWeek;
ex.DaysOfMonth = update.DaysOfMonth;
ex.MonthsOfYear = update.MonthsOfYear;

5
ErsatzTV.Application/Scheduling/Commands/UpdateDeco.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record UpdateDeco(int DecoId, int DecoGroupId, string Name, int? WatermarkId) : IRequest<Either<BaseError, DecoViewModel>>;

61
ErsatzTV.Application/Scheduling/Commands/UpdateDecoHandler.cs

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
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 UpdateDecoHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<UpdateDeco, Either<BaseError, DecoViewModel>>
{
public async Task<Either<BaseError, DecoViewModel>> Handle(UpdateDeco request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Deco> validation = await Validate(dbContext, request);
return await validation.Apply(ps => ApplyUpdateRequest(dbContext, ps, request));
}
private static async Task<DecoViewModel> ApplyUpdateRequest(
TvContext dbContext,
Deco existing,
UpdateDeco request)
{
existing.Name = request.Name;
existing.WatermarkId = request.WatermarkId;
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(existing);
}
private static async Task<Validation<BaseError, Deco>> Validate(TvContext dbContext, UpdateDeco request) =>
(await DecoMustExist(dbContext, request), await ValidateDecoName(dbContext, request))
.Apply((deco, _) => deco);
private static Task<Validation<BaseError, Deco>> DecoMustExist(
TvContext dbContext,
UpdateDeco request) =>
dbContext.Decos
.SelectOneAsync(d => d.Id, d => d.Id == request.DecoId)
.Map(o => o.ToValidation<BaseError>("Deco does not exist"));
private static async Task<Validation<BaseError, string>> ValidateDecoName(
TvContext dbContext,
UpdateDeco request)
{
if (request.Name.Length > 50)
{
return BaseError.New($"Deco name \"{request.Name}\" is invalid");
}
Option<Deco> maybeExisting = await dbContext.Decos
.FirstOrDefaultAsync(
d => d.Id != request.DecoId && d.DecoGroupId == request.DecoGroupId && d.Name == request.Name)
.Map(Optional);
return maybeExisting.IsSome
? BaseError.New($"A deco named \"{request.Name}\" already exists in that deco group")
: Success<BaseError, string>(request.Name);
}
}

3
ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDeco.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record UpdateDefaultDeco(int PlayoutId, int? DecoId) : IRequest;

16
ErsatzTV.Application/Scheduling/Commands/UpdateDefaultDecoHandler.cs

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class UpdateDefaultDecoHandler(IDbContextFactory<TvContext> dbContextFactory) : IRequestHandler<UpdateDefaultDeco>
{
public async Task Handle(UpdateDefaultDeco request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await dbContext.Playouts
.Where(p => p.Id == request.PlayoutId)
.ExecuteUpdateAsync(u => u.SetProperty(p => p.DecoId, p => request.DecoId), cancellationToken);
}
}

3
ErsatzTV.Application/Scheduling/DecoGroupViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record DecoGroupViewModel(int Id, string Name, int DecoCount);

3
ErsatzTV.Application/Scheduling/DecoTemplateGroupViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record DecoTemplateGroupViewModel(int Id, string Name, int DecoTemplateCount);

3
ErsatzTV.Application/Scheduling/DecoTemplateItemViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record DecoTemplateItemViewModel(int DecoId, string DecoName, DateTime StartTime, DateTime EndTime);

3
ErsatzTV.Application/Scheduling/DecoTemplateViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record DecoTemplateViewModel(int Id, int DecoTemplateGroupId, string Name);

3
ErsatzTV.Application/Scheduling/DecoViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record DecoViewModel(int Id, int DecoGroupId, string Name, int? WatermarkId);

27
ErsatzTV.Application/Scheduling/Mapper.cs

@ -45,11 +45,38 @@ internal static class Mapper @@ -45,11 +45,38 @@ internal static class Mapper
DateTime endTime = startTime.AddMinutes(templateItem.Block.Minutes);
return new TemplateItemViewModel(templateItem.BlockId, templateItem.Block.Name, startTime, endTime);
}
internal static DecoGroupViewModel ProjectToViewModel(DecoGroup decoGroup) =>
new(decoGroup.Id, decoGroup.Name, decoGroup.Decos.Count);
internal static DecoViewModel ProjectToViewModel(Deco deco) =>
new(deco.Id, deco.DecoGroupId, deco.Name, deco.WatermarkId);
internal static DecoTemplateGroupViewModel ProjectToViewModel(DecoTemplateGroup decoTemplateGroup) =>
new(decoTemplateGroup.Id, decoTemplateGroup.Name, decoTemplateGroup.DecoTemplates.Count);
internal static DecoTemplateViewModel ProjectToViewModel(DecoTemplate decoTemplate)
{
if (decoTemplate is null)
{
return null;
}
return new DecoTemplateViewModel(decoTemplate.Id, decoTemplate.DecoTemplateGroupId, decoTemplate.Name);
}
internal static DecoTemplateItemViewModel ProjectToViewModel(DecoTemplateItem decoTemplateItem)
{
DateTime startTime = DateTime.Today.Add(decoTemplateItem.StartTime);
DateTime endTime = DateTime.Today.Add(decoTemplateItem.EndTime);
return new DecoTemplateItemViewModel(decoTemplateItem.DecoId, decoTemplateItem.Deco.Name, startTime, endTime);
}
internal static PlayoutTemplateViewModel ProjectToViewModel(PlayoutTemplate playoutTemplate) =>
new(
playoutTemplate.Id,
ProjectToViewModel(playoutTemplate.Template),
ProjectToViewModel(playoutTemplate.DecoTemplate),
playoutTemplate.Index,
playoutTemplate.DaysOfWeek,
playoutTemplate.DaysOfMonth,

1
ErsatzTV.Application/Scheduling/PlayoutTemplateViewModel.cs

@ -3,6 +3,7 @@ namespace ErsatzTV.Application.Scheduling; @@ -3,6 +3,7 @@ namespace ErsatzTV.Application.Scheduling;
public record PlayoutTemplateViewModel(
int Id,
TemplateViewModel Template,
DecoTemplateViewModel DecoTemplate,
int Index,
ICollection<DayOfWeek> DaysOfWeek,
ICollection<int> DaysOfMonth,

3
ErsatzTV.Application/Scheduling/Queries/GetAllDecoGroups.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetAllDecoGroups : IRequest<List<DecoGroupViewModel>>;

21
ErsatzTV.Application/Scheduling/Queries/GetAllDecoGroupsHandler.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetAllDecoGroupsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllDecoGroups, List<DecoGroupViewModel>>
{
public async Task<List<DecoGroupViewModel>> Handle(GetAllDecoGroups request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<DecoGroup> decoGroups = await dbContext.DecoGroups
.AsNoTracking()
.Include(g => g.Decos)
.ToListAsync(cancellationToken);
return decoGroups.Map(Mapper.ProjectToViewModel).ToList();
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetAllDecoTemplateGroups.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetAllDecoTemplateGroups : IRequest<List<DecoTemplateGroupViewModel>>;

23
ErsatzTV.Application/Scheduling/Queries/GetAllDecoTemplateGroupsHandler.cs

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetAllDecoTemplateGroupsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllDecoTemplateGroups, List<DecoTemplateGroupViewModel>>
{
public async Task<List<DecoTemplateGroupViewModel>> Handle(
GetAllDecoTemplateGroups request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<DecoTemplateGroup> blockGroups = await dbContext.DecoTemplateGroups
.AsNoTracking()
.Include(g => g.DecoTemplates)
.ToListAsync(cancellationToken);
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecoById.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetDecoById(int DecoId) : IRequest<Option<DecoViewModel>>;

17
ErsatzTV.Application/Scheduling/Queries/GetDecoByIdHandler.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetDecoByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetDecoById, Option<DecoViewModel>>
{
public async Task<Option<DecoViewModel>> Handle(GetDecoById request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Decos
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoId)
.MapT(Mapper.ProjectToViewModel);
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutId.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetDecoByPlayoutId(int PlayoutId) : IRequest<Option<DecoViewModel>>;

18
ErsatzTV.Application/Scheduling/Queries/GetDecoByPlayoutIdHandler.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetDecoByPlayoutIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetDecoByPlayoutId, Option<DecoViewModel>>
{
public async Task<Option<DecoViewModel>> Handle(GetDecoByPlayoutId request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Playouts
.Include(p => p.Deco)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId && p.DecoId != null)
.MapT(p => Mapper.ProjectToViewModel(p.Deco));
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateById.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetDecoTemplateById(int DecoTemplateId) : IRequest<Option<DecoTemplateViewModel>>;

17
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateByIdHandler.cs

@ -0,0 +1,17 @@ @@ -0,0 +1,17 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetDecoTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetDecoTemplateById, Option<DecoTemplateViewModel>>
{
public async Task<Option<DecoTemplateViewModel>> Handle(GetDecoTemplateById request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.DecoTemplates
.SelectOneAsync(b => b.Id, b => b.Id == request.DecoTemplateId)
.MapT(Mapper.ProjectToViewModel);
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateItems.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetDecoTemplateItems(int DecoTemplateId) : IRequest<List<DecoTemplateItemViewModel>>;

20
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplateItemsHandler.cs

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetDecoTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetDecoTemplateItems, List<DecoTemplateItemViewModel>>
{
public async Task<List<DecoTemplateItemViewModel>> Handle(GetDecoTemplateItems request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.DecoTemplateItems
.AsNoTracking()
.Filter(i => i.DecoTemplateId == request.DecoTemplateId)
.Include(i => i.Deco)
.ToListAsync(cancellationToken)
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplatesByDecoTemplateGroupId.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetDecoTemplatesByDecoTemplateGroupId(int DecoTemplateGroupId) : IRequest<List<DecoTemplateViewModel>>;

21
ErsatzTV.Application/Scheduling/Queries/GetDecoTemplatesByDecoTemplateGroupIdHandler.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetDecoTemplatesByDecoTemplateGroupIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetDecoTemplatesByDecoTemplateGroupId, List<DecoTemplateViewModel>>
{
public async Task<List<DecoTemplateViewModel>> Handle(
GetDecoTemplatesByDecoTemplateGroupId request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.DecoTemplates
.AsNoTracking()
.Filter(i => i.DecoTemplateGroupId == request.DecoTemplateGroupId)
.ToListAsync(cancellationToken)
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
}
}

3
ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupId.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetDecosByDecoGroupId(int DecoGroupId) : IRequest<List<DecoViewModel>>;

21
ErsatzTV.Application/Scheduling/Queries/GetDecosByDecoGroupIdHandler.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetDecosByDecoGroupIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetDecosByDecoGroupId, List<DecoViewModel>>
{
public async Task<List<DecoViewModel>> Handle(GetDecosByDecoGroupId request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Deco> decos = await dbContext.Decos
.Filter(b => b.DecoGroupId == request.DecoGroupId)
.AsNoTracking()
.ToListAsync(cancellationToken);
return decos.Map(Mapper.ProjectToViewModel).ToList();
}
}

1
ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplatesHandler.cs

@ -17,6 +17,7 @@ public class GetPlayoutTemplatesHandler(IDbContextFactory<TvContext> dbContextFa @@ -17,6 +17,7 @@ public class GetPlayoutTemplatesHandler(IDbContextFactory<TvContext> dbContextFa
.AsNoTracking()
.Filter(t => t.PlayoutId == request.PlayoutId)
.Include(t => t.Template)
.Include(t => t.DecoTemplate)
.ToListAsync(cancellationToken);
return playoutTemplates.Map(Mapper.ProjectToViewModel).ToList();

66
ErsatzTV.Application/Streaming/Queries/GetPlayoutItemProcessByChannelNumberHandler.cs

@ -4,6 +4,7 @@ using ErsatzTV.Application.Playouts; @@ -4,6 +4,7 @@ using ErsatzTV.Application.Playouts;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.FFmpeg;
@ -78,6 +79,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -78,6 +79,9 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
DateTimeOffset now = request.Now;
Either<BaseError, PlayoutItemWithPath> maybePlayoutItem = await dbContext.PlayoutItems
.Include(i => i.Playout)
.ThenInclude(p => p.Deco)
.ThenInclude(d => d.Watermark)
.Include(i => i.MediaItem)
.ThenInclude(mi => (mi as Episode).EpisodeMetadata)
.ThenInclude(em => em.Subtitles)
@ -194,12 +198,70 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -194,12 +198,70 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
watermarkId => dbContext.ChannelWatermarks
.SelectOneAsync(w => w.Id, w => w.Id == watermarkId));
Option<ChannelWatermark> playoutItemWatermark = Optional(playoutItemWithPath.PlayoutItem.Watermark);
if (playoutItemWatermark.IsNone &&
playoutItemWithPath.PlayoutItem.Playout.ProgramSchedulePlayoutType is ProgramSchedulePlayoutType.Block)
{
_logger.LogDebug("Block playout item has no watermark; checking for deco");
// check for playout template deco
// load all playout templates
// get playout template for start time
// check for deco
// load all templates that have decos
List<PlayoutTemplate> playoutTemplates = await dbContext.PlayoutTemplates
.AsNoTracking()
.Filter(t => t.PlayoutId == playoutItemWithPath.PlayoutItem.PlayoutId)
.Filter(t => t.DecoTemplateId != null)
.Include(t => t.DecoTemplate)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Deco)
.ThenInclude(d => d.Watermark)
.ToListAsync(cancellationToken);
Option<PlayoutTemplate> maybeActiveTemplate = PlayoutTemplateSelector.GetPlayoutTemplateFor(
playoutTemplates,
playoutItemWithPath.PlayoutItem.StartOffset);
foreach (PlayoutTemplate activeTemplate in maybeActiveTemplate)
{
_logger.LogDebug("Block playout has active playout template; checking for deco template items");
Option<DecoTemplateItem> maybeItem = activeTemplate.DecoTemplate.Items
.Find(i => i.StartTime <= now.TimeOfDay && i.EndTime > now.TimeOfDay);
foreach (DecoTemplateItem item in maybeItem)
{
_logger.LogDebug("Block playout has active deco template item; checking for watermark");
foreach (ChannelWatermark watermark in Optional(item.Deco.Watermark))
{
_logger.LogDebug(
"Block playout has active deco template item with watermark; will use for this playout item");
playoutItemWatermark = watermark;
}
}
}
if (playoutItemWatermark.IsNone)
{
// check for playout deco
foreach (Deco deco in Optional(playoutItemWithPath.PlayoutItem.Playout.Deco))
{
_logger.LogDebug("Block playout item has default deco; checking for watermark");
foreach (ChannelWatermark watermark in Optional(deco.Watermark))
{
_logger.LogDebug(
"Block playout has default deco with watermark; will use for this playout item");
playoutItemWatermark = watermark;
}
}
}
}
if (playoutItemWithPath.PlayoutItem.MediaItem is Song song)
{
(videoPath, videoVersion) = await _songVideoGenerator.GenerateSongVideo(
song,
channel,
Optional(playoutItemWithPath.PlayoutItem.Watermark),
playoutItemWatermark,
maybeGlobalWatermark,
ffmpegPath,
ffprobePath,
@ -239,7 +301,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler< @@ -239,7 +301,7 @@ public class GetPlayoutItemProcessByChannelNumberHandler : FFmpegProcessHandler<
start,
finish,
request.StartAtZero ? start : now,
Optional(playoutItemWithPath.PlayoutItem.Watermark),
playoutItemWatermark,
maybeGlobalWatermark,
channel.FFmpegProfile.VaapiDriver,
channel.FFmpegProfile.VaapiDevice,

2
ErsatzTV.Core/Domain/Playout.cs

@ -20,4 +20,6 @@ public class Playout @@ -20,4 +20,6 @@ public class Playout
public ICollection<PlayoutHistory> PlayoutHistory { get; set; }
public int Seed { get; set; }
public TimeSpan? DailyRebuildTime { get; set; }
public int? DecoId { get; set; }
public Deco Deco { get; set; }
}

16
ErsatzTV.Core/Domain/Scheduling/Deco.cs

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class Deco
{
public int Id { get; set; }
public int DecoGroupId { get; set; }
public DecoGroup DecoGroup { get; set; }
public string Name { get; set; }
public int? WatermarkId { get; set; }
public ChannelWatermark Watermark { get; set; }
// can be added directly to playouts
public ICollection<Playout> Playouts { get; set; }
}

8
ErsatzTV.Core/Domain/Scheduling/DecoGroup.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class DecoGroup
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Deco> Decos { get; set; }
}

12
ErsatzTV.Core/Domain/Scheduling/DecoTemplate.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class DecoTemplate
{
public int Id { get; set; }
public int DecoTemplateGroupId { get; set; }
public DecoTemplateGroup DecoTemplateGroup { get; set; }
public string Name { get; set; }
public ICollection<DecoTemplateItem> Items { get; set; }
public ICollection<PlayoutTemplate> PlayoutTemplates { get; set; }
public DateTime DateUpdated { get; set; }
}

8
ErsatzTV.Core/Domain/Scheduling/DecoTemplateGroup.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class DecoTemplateGroup
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<DecoTemplate> DecoTemplates { get; set; }
}

12
ErsatzTV.Core/Domain/Scheduling/DecoTemplateItem.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class DecoTemplateItem
{
public int Id { get; set; }
public int DecoTemplateId { get; set; }
public DecoTemplate DecoTemplate { get; set; }
public int DecoId { get; set; }
public Deco Deco { get; set; }
public TimeSpan StartTime { get; set; }
public TimeSpan EndTime { get; set; }
}

2
ErsatzTV.Core/Domain/Scheduling/PlayoutTemplate.cs

@ -7,6 +7,8 @@ public class PlayoutTemplate @@ -7,6 +7,8 @@ public class PlayoutTemplate
public Playout Playout { get; set; }
public int TemplateId { get; set; }
public Template Template { get; set; }
public int? DecoTemplateId { get; set; }
public DecoTemplate DecoTemplate { get; set; }
public int Index { get; set; }
public ICollection<DayOfWeek> DaysOfWeek { get; set; }
public ICollection<int> DaysOfMonth { get; set; }

5404
ErsatzTV.Infrastructure.MySql/Migrations/20240404160439_Add_Deco_Group_Deco.Designer.cs generated

File diff suppressed because it is too large Load Diff

117
ErsatzTV.Infrastructure.MySql/Migrations/20240404160439_Add_Deco_Group_Deco.cs

@ -0,0 +1,117 @@ @@ -0,0 +1,117 @@
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_Deco_Group_Deco : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DecoId",
table: "Playout",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "DecoGroup",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_DecoGroup", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Deco",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
DecoGroupId = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
WatermarkId = table.Column<int>(type: "int", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Deco", x => x.Id);
table.ForeignKey(
name: "FK_Deco_ChannelWatermark_WatermarkId",
column: x => x.WatermarkId,
principalTable: "ChannelWatermark",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Deco_DecoGroup_DecoGroupId",
column: x => x.DecoGroupId,
principalTable: "DecoGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Playout_DecoId",
table: "Playout",
column: "DecoId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DecoGroupId_Name",
table: "Deco",
columns: new[] { "DecoGroupId", "Name" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Deco_WatermarkId",
table: "Deco",
column: "WatermarkId");
migrationBuilder.CreateIndex(
name: "IX_DecoGroup_Name",
table: "DecoGroup",
column: "Name",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_Playout_Deco_DecoId",
table: "Playout",
column: "DecoId",
principalTable: "Deco",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Playout_Deco_DecoId",
table: "Playout");
migrationBuilder.DropTable(
name: "Deco");
migrationBuilder.DropTable(
name: "DecoGroup");
migrationBuilder.DropIndex(
name: "IX_Playout_DecoId",
table: "Playout");
migrationBuilder.DropColumn(
name: "DecoId",
table: "Playout");
}
}
}

5533
ErsatzTV.Infrastructure.MySql/Migrations/20240404182555_Add_DecoTemplate.Designer.cs generated

File diff suppressed because it is too large Load Diff

154
ErsatzTV.Infrastructure.MySql/Migrations/20240404182555_Add_DecoTemplate.cs

@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_DecoTemplate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DecoTemplateId",
table: "PlayoutTemplate",
type: "int",
nullable: true);
migrationBuilder.CreateTable(
name: "DecoTemplateGroup",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_DecoTemplateGroup", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "DecoTemplate",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
DecoTemplateGroupId = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
DateUpdated = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoTemplate", x => x.Id);
table.ForeignKey(
name: "FK_DecoTemplate_DecoTemplateGroup_DecoTemplateGroupId",
column: x => x.DecoTemplateGroupId,
principalTable: "DecoTemplateGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "DecoTemplateItem",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
DecoTemplateId = table.Column<int>(type: "int", nullable: false),
DecoId = table.Column<int>(type: "int", nullable: false),
StartTime = table.Column<TimeSpan>(type: "time(6)", nullable: false),
EndTime = table.Column<TimeSpan>(type: "time(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoTemplateItem", x => x.Id);
table.ForeignKey(
name: "FK_DecoTemplateItem_DecoTemplate_DecoTemplateId",
column: x => x.DecoTemplateId,
principalTable: "DecoTemplate",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DecoTemplateItem_Deco_DecoId",
column: x => x.DecoId,
principalTable: "Deco",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_PlayoutTemplate_DecoTemplateId",
table: "PlayoutTemplate",
column: "DecoTemplateId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId",
table: "DecoTemplate",
column: "DecoTemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_Name",
table: "DecoTemplate",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DecoTemplateGroup_Name",
table: "DecoTemplateGroup",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DecoTemplateItem_DecoId",
table: "DecoTemplateItem",
column: "DecoId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplateItem_DecoTemplateId",
table: "DecoTemplateItem",
column: "DecoTemplateId");
migrationBuilder.AddForeignKey(
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
table: "PlayoutTemplate",
column: "DecoTemplateId",
principalTable: "DecoTemplate",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
table: "PlayoutTemplate");
migrationBuilder.DropTable(
name: "DecoTemplateItem");
migrationBuilder.DropTable(
name: "DecoTemplate");
migrationBuilder.DropTable(
name: "DecoTemplateGroup");
migrationBuilder.DropIndex(
name: "IX_PlayoutTemplate_DecoTemplateId",
table: "PlayoutTemplate");
migrationBuilder.DropColumn(
name: "DecoTemplateId",
table: "PlayoutTemplate");
}
}
}

215
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -1590,6 +1590,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -1590,6 +1590,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<TimeSpan?>("DailyRebuildTime")
.HasColumnType("time(6)");
b.Property<int?>("DecoId")
.HasColumnType("int");
b.Property<string>("ExternalJsonFile")
.HasColumnType("longtext");
@ -1606,6 +1609,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -1606,6 +1609,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasIndex("ChannelId");
b.HasIndex("DecoId");
b.HasIndex("ProgramScheduleId");
b.ToTable("Playout", (string)null);
@ -2132,6 +2137,127 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2132,6 +2137,127 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.ToTable("BlockItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<int>("DecoGroupId")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<int?>("WatermarkId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
.IsUnique();
b.ToTable("Deco", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("DecoGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<DateTime>("DateUpdated")
.HasColumnType("datetime(6)");
b.Property<int>("DecoTemplateGroupId")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("DecoTemplateGroupId");
b.HasIndex("Name")
.IsUnique();
b.ToTable("DecoTemplate", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("DecoTemplateGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<int>("DecoId")
.HasColumnType("int");
b.Property<int>("DecoTemplateId")
.HasColumnType("int");
b.Property<TimeSpan>("EndTime")
.HasColumnType("time(6)");
b.Property<TimeSpan>("StartTime")
.HasColumnType("time(6)");
b.HasKey("Id");
b.HasIndex("DecoId");
b.HasIndex("DecoTemplateId");
b.ToTable("DecoTemplateItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.Property<int>("Id")
@ -2187,6 +2313,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2187,6 +2313,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<string>("DaysOfWeek")
.HasColumnType("longtext");
b.Property<int?>("DecoTemplateId")
.HasColumnType("int");
b.Property<int>("EndDay")
.HasColumnType("int");
@ -2216,6 +2345,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -2216,6 +2345,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.HasKey("Id");
b.HasIndex("DecoTemplateId");
b.HasIndex("PlayoutId");
b.HasIndex("TemplateId");
@ -3908,6 +4039,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -3908,6 +4039,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany("Playouts")
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("Playouts")
.HasForeignKey("ProgramScheduleId")
@ -3969,6 +4105,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -3969,6 +4105,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("Channel");
b.Navigation("Deco");
b.Navigation("ProgramSchedule");
});
@ -4269,6 +4407,54 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4269,6 +4407,54 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("SmartCollection");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoGroup", "DecoGroup")
.WithMany("Decos")
.HasForeignKey("DecoGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DecoGroup");
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", "DecoTemplateGroup")
.WithMany("DecoTemplates")
.HasForeignKey("DecoTemplateGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DecoTemplateGroup");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany()
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
.WithMany("Items")
.HasForeignKey("DecoTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Deco");
b.Navigation("DecoTemplate");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
@ -4290,6 +4476,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4290,6 +4476,11 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
.WithMany("PlayoutTemplates")
.HasForeignKey("DecoTemplateId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Templates")
.HasForeignKey("PlayoutId")
@ -4302,6 +4493,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4302,6 +4493,8 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DecoTemplate");
b.Navigation("Playout");
b.Navigation("Template");
@ -5138,6 +5331,28 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -5138,6 +5331,28 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("Blocks");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
{
b.Navigation("Decos");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
{
b.Navigation("Items");
b.Navigation("PlayoutTemplates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
{
b.Navigation("DecoTemplates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.Navigation("Items");

5255
ErsatzTV.Infrastructure.Sqlite/Migrations/20240403230520_Add_Deco_Group_Deco.Designer.cs generated

File diff suppressed because it is too large Load Diff

112
ErsatzTV.Infrastructure.Sqlite/Migrations/20240403230520_Add_Deco_Group_Deco.cs

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Deco_Group_Deco : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DecoId",
table: "Playout",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateTable(
name: "DecoGroup",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoGroup", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Deco",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DecoGroupId = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
WatermarkId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Deco", x => x.Id);
table.ForeignKey(
name: "FK_Deco_ChannelWatermark_WatermarkId",
column: x => x.WatermarkId,
principalTable: "ChannelWatermark",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Deco_DecoGroup_DecoGroupId",
column: x => x.DecoGroupId,
principalTable: "DecoGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Playout_DecoId",
table: "Playout",
column: "DecoId");
migrationBuilder.CreateIndex(
name: "IX_Deco_DecoGroupId_Name",
table: "Deco",
columns: new[] { "DecoGroupId", "Name" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Deco_WatermarkId",
table: "Deco",
column: "WatermarkId");
migrationBuilder.CreateIndex(
name: "IX_DecoGroup_Name",
table: "DecoGroup",
column: "Name",
unique: true);
migrationBuilder.AddForeignKey(
name: "FK_Playout_Deco_DecoId",
table: "Playout",
column: "DecoId",
principalTable: "Deco",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Playout_Deco_DecoId",
table: "Playout");
migrationBuilder.DropTable(
name: "Deco");
migrationBuilder.DropTable(
name: "DecoGroup");
migrationBuilder.DropIndex(
name: "IX_Playout_DecoId",
table: "Playout");
migrationBuilder.DropColumn(
name: "DecoId",
table: "Playout");
}
}
}

5375
ErsatzTV.Infrastructure.Sqlite/Migrations/20240404180108_Add_DecoTemplate.Designer.cs generated

File diff suppressed because it is too large Load Diff

147
ErsatzTV.Infrastructure.Sqlite/Migrations/20240404180108_Add_DecoTemplate.cs

@ -0,0 +1,147 @@ @@ -0,0 +1,147 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_DecoTemplate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DecoTemplateId",
table: "PlayoutTemplate",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateTable(
name: "DecoTemplateGroup",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoTemplateGroup", x => x.Id);
});
migrationBuilder.CreateTable(
name: "DecoTemplate",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DecoTemplateGroupId = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
DateUpdated = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoTemplate", x => x.Id);
table.ForeignKey(
name: "FK_DecoTemplate_DecoTemplateGroup_DecoTemplateGroupId",
column: x => x.DecoTemplateGroupId,
principalTable: "DecoTemplateGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "DecoTemplateItem",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DecoTemplateId = table.Column<int>(type: "INTEGER", nullable: false),
DecoId = table.Column<int>(type: "INTEGER", nullable: false),
StartTime = table.Column<TimeSpan>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DecoTemplateItem", x => x.Id);
table.ForeignKey(
name: "FK_DecoTemplateItem_DecoTemplate_DecoTemplateId",
column: x => x.DecoTemplateId,
principalTable: "DecoTemplate",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DecoTemplateItem_Deco_DecoId",
column: x => x.DecoId,
principalTable: "Deco",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PlayoutTemplate_DecoTemplateId",
table: "PlayoutTemplate",
column: "DecoTemplateId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_DecoTemplateGroupId",
table: "DecoTemplate",
column: "DecoTemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplate_Name",
table: "DecoTemplate",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DecoTemplateGroup_Name",
table: "DecoTemplateGroup",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_DecoTemplateItem_DecoId",
table: "DecoTemplateItem",
column: "DecoId");
migrationBuilder.CreateIndex(
name: "IX_DecoTemplateItem_DecoTemplateId",
table: "DecoTemplateItem",
column: "DecoTemplateId");
migrationBuilder.AddForeignKey(
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
table: "PlayoutTemplate",
column: "DecoTemplateId",
principalTable: "DecoTemplate",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_PlayoutTemplate_DecoTemplate_DecoTemplateId",
table: "PlayoutTemplate");
migrationBuilder.DropTable(
name: "DecoTemplateItem");
migrationBuilder.DropTable(
name: "DecoTemplate");
migrationBuilder.DropTable(
name: "DecoTemplateGroup");
migrationBuilder.DropIndex(
name: "IX_PlayoutTemplate_DecoTemplateId",
table: "PlayoutTemplate");
migrationBuilder.DropColumn(
name: "DecoTemplateId",
table: "PlayoutTemplate");
}
}
}

5378
ErsatzTV.Infrastructure.Sqlite/Migrations/20240404182159_Add_DecoTemplateItem_EndTime.Designer.cs generated

File diff suppressed because it is too large Load Diff

30
ErsatzTV.Infrastructure.Sqlite/Migrations/20240404182159_Add_DecoTemplateItem_EndTime.cs

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_DecoTemplateItem_EndTime : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<TimeSpan>(
name: "EndTime",
table: "DecoTemplateItem",
type: "TEXT",
nullable: false,
defaultValue: new TimeSpan(0, 0, 0, 0, 0));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EndTime",
table: "DecoTemplateItem");
}
}
}

205
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -1509,6 +1509,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1509,6 +1509,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<TimeSpan?>("DailyRebuildTime")
.HasColumnType("TEXT");
b.Property<int?>("DecoId")
.HasColumnType("INTEGER");
b.Property<string>("ExternalJsonFile")
.HasColumnType("TEXT");
@ -1525,6 +1528,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1525,6 +1528,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("ChannelId");
b.HasIndex("DecoId");
b.HasIndex("ProgramScheduleId");
b.ToTable("Playout", (string)null);
@ -2025,6 +2030,117 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2025,6 +2030,117 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.ToTable("BlockItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DecoGroupId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("WatermarkId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("WatermarkId");
b.HasIndex("DecoGroupId", "Name")
.IsUnique();
b.ToTable("Deco", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("DecoGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateUpdated")
.HasColumnType("TEXT");
b.Property<int>("DecoTemplateGroupId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DecoTemplateGroupId");
b.HasIndex("Name")
.IsUnique();
b.ToTable("DecoTemplate", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("DecoTemplateGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DecoId")
.HasColumnType("INTEGER");
b.Property<int>("DecoTemplateId")
.HasColumnType("INTEGER");
b.Property<TimeSpan>("EndTime")
.HasColumnType("TEXT");
b.Property<TimeSpan>("StartTime")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DecoId");
b.HasIndex("DecoTemplateId");
b.ToTable("DecoTemplateItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.Property<int>("Id")
@ -2076,6 +2192,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2076,6 +2192,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<string>("DaysOfWeek")
.HasColumnType("TEXT");
b.Property<int?>("DecoTemplateId")
.HasColumnType("INTEGER");
b.Property<int>("EndDay")
.HasColumnType("INTEGER");
@ -2105,6 +2224,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2105,6 +2224,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasKey("Id");
b.HasIndex("DecoTemplateId");
b.HasIndex("PlayoutId");
b.HasIndex("TemplateId");
@ -3763,6 +3884,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3763,6 +3884,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany("Playouts")
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.ProgramSchedule", "ProgramSchedule")
.WithMany("Playouts")
.HasForeignKey("ProgramScheduleId")
@ -3824,6 +3950,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3824,6 +3950,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("Channel");
b.Navigation("Deco");
b.Navigation("ProgramSchedule");
});
@ -4124,6 +4252,54 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4124,6 +4252,54 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("SmartCollection");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoGroup", "DecoGroup")
.WithMany("Decos")
.HasForeignKey("DecoGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.ChannelWatermark", "Watermark")
.WithMany()
.HasForeignKey("WatermarkId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DecoGroup");
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", "DecoTemplateGroup")
.WithMany("DecoTemplates")
.HasForeignKey("DecoTemplateGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DecoTemplateGroup");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Deco", "Deco")
.WithMany()
.HasForeignKey("DecoId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
.WithMany("Items")
.HasForeignKey("DecoTemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Deco");
b.Navigation("DecoTemplate");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
@ -4145,6 +4321,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4145,6 +4321,11 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", "DecoTemplate")
.WithMany("PlayoutTemplates")
.HasForeignKey("DecoTemplateId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Templates")
.HasForeignKey("PlayoutId")
@ -4157,6 +4338,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4157,6 +4338,8 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("DecoTemplate");
b.Navigation("Playout");
b.Navigation("Template");
@ -4993,6 +5176,28 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4993,6 +5176,28 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("Blocks");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Deco", b =>
{
b.Navigation("Playouts");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoGroup", b =>
{
b.Navigation("Decos");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplate", b =>
{
b.Navigation("Items");
b.Navigation("PlayoutTemplates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.DecoTemplateGroup", b =>
{
b.Navigation("DecoTemplates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.Navigation("Items");

28
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoConfiguration.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class DecoConfiguration : IEntityTypeConfiguration<Deco>
{
public void Configure(EntityTypeBuilder<Deco> builder)
{
builder.ToTable("Deco");
builder.HasIndex(d => new { d.DecoGroupId, d.Name })
.IsUnique();
builder.HasMany(d => d.Playouts)
.WithOne(p => p.Deco)
.HasForeignKey(p => p.DecoId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
builder.HasOne(d => d.Watermark)
.WithMany()
.HasForeignKey(d => d.WatermarkId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
}
}

21
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoGroupConfiguration.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class DecoGroupConfiguration : IEntityTypeConfiguration<DecoGroup>
{
public void Configure(EntityTypeBuilder<DecoGroup> builder)
{
builder.ToTable("DecoGroup");
builder.HasIndex(dg => dg.Name)
.IsUnique();
builder.HasMany(dg => dg.Decos)
.WithOne(d => d.DecoGroup)
.HasForeignKey(d => d.DecoGroupId)
.OnDelete(DeleteBehavior.Cascade);
}
}

27
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateConfiguration.cs

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class DecoTemplateConfiguration : IEntityTypeConfiguration<DecoTemplate>
{
public void Configure(EntityTypeBuilder<DecoTemplate> builder)
{
builder.ToTable("DecoTemplate");
builder.HasIndex(b => b.Name)
.IsUnique();
builder.HasMany(b => b.Items)
.WithOne(i => i.DecoTemplate)
.HasForeignKey(i => i.DecoTemplateId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(t => t.PlayoutTemplates)
.WithOne(t => t.DecoTemplate)
.HasForeignKey(t => t.DecoTemplateId)
.OnDelete(DeleteBehavior.SetNull)
.IsRequired(false);
}
}

21
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateGroupConfiguration.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class DecoTemplateGroupConfiguration : IEntityTypeConfiguration<DecoTemplateGroup>
{
public void Configure(EntityTypeBuilder<DecoTemplateGroup> builder)
{
builder.ToTable("DecoTemplateGroup");
builder.HasIndex(b => b.Name)
.IsUnique();
builder.HasMany(b => b.DecoTemplates)
.WithOne(i => i.DecoTemplateGroup)
.HasForeignKey(i => i.DecoTemplateGroupId)
.OnDelete(DeleteBehavior.Cascade);
}
}

10
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/DecoTemplateItemConfiguration.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class DecoTemplateItemConfiguration : IEntityTypeConfiguration<DecoTemplateItem>
{
public void Configure(EntityTypeBuilder<DecoTemplateItem> builder) => builder.ToTable("DecoTemplateItem");
}

17
ErsatzTV.Infrastructure/Data/Repositories/MediaSourceRepository.cs

@ -120,7 +120,22 @@ public class MediaSourceRepository : IMediaSourceRepository @@ -120,7 +120,22 @@ public class MediaSourceRepository : IMediaSourceRepository
}
}
await dbContext.SaveChangesAsync();
// TODO: possibly caused by https://github.com/dotnet/efcore/issues/33133
var success = false;
var attempts = 0;
while (!success && attempts < 3)
{
try
{
await dbContext.SaveChangesAsync();
success = true;
}
catch (DbUpdateException)
{
// do nothing
attempts++;
}
}
}
public async Task<List<int>> UpdateLibraries(

5
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -94,6 +94,11 @@ public class TvContext : DbContext @@ -94,6 +94,11 @@ public class TvContext : DbContext
public DbSet<TemplateGroup> TemplateGroups { get; set; }
public DbSet<Template> Templates { get; set; }
public DbSet<TemplateItem> TemplateItems { get; set; }
public DbSet<DecoGroup> DecoGroups { get; set; }
public DbSet<Deco> Decos { get; set; }
public DbSet<DecoTemplateGroup> DecoTemplateGroups { get; set; }
public DbSet<DecoTemplate> DecoTemplates { get; set; }
public DbSet<DecoTemplateItem> DecoTemplateItems { get; set; }
public DbSet<FFmpegProfile> FFmpegProfiles { get; set; }
public DbSet<Resolution> Resolutions { get; set; }
public DbSet<LanguageCode> LanguageCodes { get; set; }

157
ErsatzTV/Pages/BlockPlayoutEditor.razor

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
@page "/playouts/block/{Id:int}"
@using ErsatzTV.Application.Scheduling
@using ErsatzTV.Application.Channels
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<BlockPlayoutEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IEntityLocker EntityLocker;
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Edit Block Playout - @_channelName</MudText>
<MudGrid Class="mt-4">
<MudCard Class="mr-6 mb-6" Style="width: 400px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Playout Templates</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Primary" Link="@($"playouts/{Id}/templates")" Class="mt-4">
Edit Templates
</MudButton>
</MudCardContent>
</MudCard>
<MudCard Class="mr-6 mb-6" Style="width: 400px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Playout Items and History</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Warning" OnClick="@(_ => EraseItems(eraseHistory: false))" Class="mt-4">
Erase Items
</MudButton>
</div>
<div>
<MudButton Disabled="@EntityLocker.IsPlayoutLocked(Id)" Variant="Variant.Filled" Color="Color.Error" OnClick="@(_ => EraseItems(eraseHistory: true))" Class="mt-4">
Erase Items and History
</MudButton>
</div>
</MudCardContent>
</MudCard>
<MudCard Class="mr-6 mb-6" Style="width: 400px">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Default Deco</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudElement HtmlTag="div" Class="mt-3">
<MudSwitch T="bool" Label="Enable Default Deco" @bind-Value="_enableDefaultDeco" Color="Color.Primary"/>
</MudElement>
@if (_enableDefaultDeco)
{
<MudElement HtmlTag="div" Class="mt-2">
<MudSelect T="DecoGroupViewModel"
Label="Deco Group"
Value="@_selectedDefaultDecoGroup"
ValueChanged="@(vm => UpdateDefaultDecoTemplateGroupItems(vm))">
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
{
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
}
</MudSelect>
</MudElement>
<MudElement HtmlTag="div" Class="mt-2">
<MudSelect Label="Deco"
@bind-Value="_defaultDeco"
For="@(() => _defaultDeco)">
@foreach (DecoViewModel deco in _decos)
{
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
}
</MudSelect>
</MudElement>
}
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveDefaultDeco())">
Save Changes
</MudButton>
</MudCardActions>
</MudCard>
</MudGrid>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int Id { get; set; }
private readonly List<DecoGroupViewModel> _decoGroups = [];
private readonly List<DecoViewModel> _decos = [];
private string _channelName;
private bool _enableDefaultDeco;
private DecoGroupViewModel _selectedDefaultDecoGroup;
private DecoViewModel _defaultDeco;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<string> maybeName = await Mediator.Send(new GetChannelNameByPlayoutId(Id), _cts.Token);
if (maybeName.IsNone)
{
NavigationManager.NavigateTo("/playouts");
return;
}
foreach (string name in maybeName)
{
_channelName = name;
}
_decoGroups.Clear();
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token));
Option<DecoViewModel> maybeDefaultDeco = await Mediator.Send(new GetDecoByPlayoutId(Id), _cts.Token);
foreach (DecoViewModel defaultDeco in maybeDefaultDeco)
{
_enableDefaultDeco = true;
_selectedDefaultDecoGroup = _decoGroups.SingleOrDefault(dg => dg.Id == defaultDeco.DecoGroupId);
await UpdateDefaultDecoTemplateGroupItems(_selectedDefaultDecoGroup);
_defaultDeco = defaultDeco;
}
}
private async Task UpdateDefaultDecoTemplateGroupItems(DecoGroupViewModel decoGroup)
{
_selectedDefaultDecoGroup = decoGroup;
_decos.Clear();
_decos.AddRange(await Mediator.Send(new GetDecosByDecoGroupId(_selectedDefaultDecoGroup.Id), _cts.Token));
}
private async Task EraseItems(bool eraseHistory)
{
IRequest request = eraseHistory ? new EraseBlockPlayoutHistory(Id) : new EraseBlockPlayoutItems(Id);
await Mediator.Send(request, _cts.Token);
string message = eraseHistory ? "Erased playout items and history" : "Erased playout items";
Snackbar.Add(message, Severity.Info);
}
private async Task SaveDefaultDeco()
{
int? decoId = _enableDefaultDeco ? _defaultDeco?.Id : null;
await Mediator.Send(new UpdateDefaultDeco(Id, decoId), _cts.Token);
}
}

90
ErsatzTV/Pages/DecoEditor.razor

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
@page "/decos/{Id:int}"
@using ErsatzTV.Application.Scheduling
@using ErsatzTV.Application.Watermarks
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<DecoEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Edit Deco</MudText>
<div style="max-width: 400px">
<MudCard>
<MudCardContent>
<MudTextField Label="Name" @bind-Value="_deco.Name" For="@(() => _deco.Name)"/>
<MudSelect Class="mt-3" Label="Watermark" @bind-Value="_deco.WatermarkId" For="@(() => _deco.WatermarkId)"
Clearable="true">
<MudSelectItem T="int?" Value="@((int?)null)">(none)</MudSelectItem>
@foreach (WatermarkViewModel watermark in _watermarks)
{
<MudSelectItem T="int?" Value="@watermark.Id">@watermark.Name</MudSelectItem>
}
</MudSelect>
</MudCardContent>
</MudCard>
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4 ml-4">
Save Changes
</MudButton>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int Id { get; set; }
private DecoEditViewModel _deco = new();
private List<WatermarkViewModel> _watermarks = [];
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadWatermarks();
await LoadDeco();
}
private async Task LoadWatermarks() =>
_watermarks = await Mediator.Send(new GetAllWatermarks(), _cts.Token);
private async Task LoadDeco()
{
Option<DecoViewModel> maybeDeco = await Mediator.Send(new GetDecoById(Id), _cts.Token);
if (maybeDeco.IsNone)
{
NavigationManager.NavigateTo("decos");
return;
}
foreach (DecoViewModel deco in maybeDeco)
{
_deco = new DecoEditViewModel
{
Name = deco.Name,
DecoGroupId = deco.DecoGroupId,
WatermarkId = deco.WatermarkId
};
}
}
private async Task SaveChanges()
{
Seq<BaseError> errorMessages = await Mediator
.Send(new UpdateDeco(Id, _deco.DecoGroupId, _deco.Name, _deco.WatermarkId), _cts.Token)
.Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match(
error =>
{
Snackbar.Add($"Unexpected error saving deco: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving deco: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("/decos"));
}
}

314
ErsatzTV/Pages/DecoTemplateEditor.razor

@ -0,0 +1,314 @@ @@ -0,0 +1,314 @@
@page "/deco-templates/{Id:int}"
@using ErsatzTV.Application.Scheduling
@using System.Globalization
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<DecoTemplateEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Edit Deco Template</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px">
<MudCard>
<MudCardContent>
<MudTextField Label="Name" @bind-Value="_decoTemplate.Name" For="@(() => _decoTemplate.Name)"/>
</MudCardContent>
</MudCard>
</div>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => SaveChanges())" Class="mt-4">
Save Changes
</MudButton>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px">
<MudCard>
<MudCardContent>
<MudSelect T="DecoGroupViewModel"
Label="Deco Group"
ValueChanged="@(vm => UpdateDecoGroupItems(vm))">
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
{
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
}
</MudSelect>
<MudSelect Class="mt-3"
T="DecoViewModel"
Label="Deco"
@bind-value="_selectedDeco">
@foreach (DecoViewModel deco in _decos)
{
<MudSelectItem Value="@deco">@deco.Name</MudSelectItem>
}
</MudSelect>
<MudSelect Class="mt-3"
T="DateTime"
Label="Start Time On Or After"
@bind-value="_selectedDecoStart">
@foreach (DateTime startTime in _startTimes)
{
<MudSelectItem Value="@startTime">
@startTime.ToString(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern)
</MudSelectItem>
}
</MudSelect>
<MudGrid Class="mt-3" Style="align-items: center" Justify="Justify.Center">
<MudItem xs="6">
<MudTextField T="int"
Label="Duration"
@bind-Value="_durationHours"
Adornment="Adornment.End"
AdornmentText="hours"/>
</MudItem>
<MudItem xs="6">
<MudSelect T="int" @bind-Value="_durationMinutes" Adornment="Adornment.End" AdornmentText="minutes">
<MudSelectItem Value="0"/>
<MudSelectItem Value="5"/>
<MudSelectItem Value="10"/>
<MudSelectItem Value="15"/>
<MudSelectItem Value="20"/>
<MudSelectItem Value="25"/>
<MudSelectItem Value="30"/>
<MudSelectItem Value="35"/>
<MudSelectItem Value="40"/>
<MudSelectItem Value="45"/>
<MudSelectItem Value="50"/>
<MudSelectItem Value="55"/>
</MudSelect>
</MudItem>
</MudGrid>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoToDecoTemplate())" Disabled="@(_selectedDeco is null)">
Add Deco To Deco Template
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px">
<MudCard>
<MudCardContent>
<MudSelect T="DecoTemplateItemEditViewModel"
Label="Deco To Remove"
@bind-Value="_decoToRemove">
<MudSelectItem Value="@((DecoTemplateItemEditViewModel)null)">(none)</MudSelectItem>
@foreach (DecoTemplateItemEditViewModel item in _decoTemplate.Items.OrderBy(i => i.Start))
{
<MudSelectItem Value="@item">@item.Start.ToShortTimeString() - @item.Text</MudSelectItem>
}
</MudSelect>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => RemoveDecoFromDecoTemplate())" Disabled="@(_decoToRemove is null)">
Remove Deco From Deco Template
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCalendar Class="mt-4"
Items="@_decoTemplate.Items"
ShowMonth="false"
ShowWeek="false"
ShowPrevNextButtons="false"
ShowDatePicker="false"
ShowTodayButton="false"
DayTimeInterval="CalendarTimeInterval.Minutes10"
Use24HourClock="@(CultureInfo.CurrentUICulture.DateTimeFormat.ShortTimePattern.Contains("H"))"
EnableDragItems="true"
EnableResizeItems="false"
ItemChanged="@(ci => CalendarItemChanged(ci))"/>
</MudItem>
</MudGrid>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private readonly List<DecoGroupViewModel> _decoGroups = [];
private readonly List<DecoViewModel> _decos = [];
private readonly List<DateTime> _startTimes = [];
[Parameter]
public int Id { get; set; }
private DecoTemplateItemsEditViewModel _decoTemplate = new();
private DecoTemplateItemEditViewModel _decoToRemove;
private DecoGroupViewModel _selectedDecoGroup;
private DecoViewModel _selectedDeco;
private DateTime _selectedDecoStart;
private int _durationHours;
private int _durationMinutes = 15;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await LoadDecoTemplateItems();
DateTime start = DateTime.Today;
_selectedDecoStart = start;
while (start.Date == DateTime.Today.Date)
{
_startTimes.Add(start);
start = start.AddMinutes(5);
}
}
private async Task LoadDecoTemplateItems()
{
Option<DecoTemplateViewModel> maybeDecoTemplate = await Mediator.Send(new GetDecoTemplateById(Id), _cts.Token);
if (maybeDecoTemplate.IsNone)
{
NavigationManager.NavigateTo("deco-templates");
return;
}
foreach (DecoTemplateViewModel template in maybeDecoTemplate)
{
_decoTemplate = new DecoTemplateItemsEditViewModel
{
Name = template.Name,
Items = []
};
}
Option<IEnumerable<DecoTemplateItemViewModel>> maybeResults = await Mediator.Send(new GetDecoTemplateItems(Id), _cts.Token);
foreach (IEnumerable<DecoTemplateItemViewModel> items in maybeResults)
{
_decoTemplate.Items.AddRange(items.Map(ProjectToEditViewModel));
}
_decoGroups.AddRange(await Mediator.Send(new GetAllDecoGroups(), _cts.Token));
}
private static DecoTemplateItemEditViewModel ProjectToEditViewModel(DecoTemplateItemViewModel item) =>
new()
{
DecoId = item.DecoId,
DecoName = item.DecoName,
Start = item.StartTime,
End = item.EndTime
};
private async Task UpdateDecoGroupItems(DecoGroupViewModel decoGroup)
{
_selectedDecoGroup = decoGroup;
_decos.Clear();
_decos.AddRange(await Mediator.Send(new GetDecosByDecoGroupId(_selectedDecoGroup.Id), _cts.Token));
}
private void AddDecoToDecoTemplate()
{
// find first time where this deco will fit
DateTime maybeStart = _selectedDecoStart;
while (maybeStart.Date == DateTime.Today)
{
DateTime maybeEnd = maybeStart.AddHours(_durationHours).AddMinutes(_durationMinutes);
if (IntersectsOthers(null, maybeStart, maybeEnd) == false)
{
var item = new DecoTemplateItemEditViewModel
{
DecoId = _selectedDeco.Id,
DecoName = _selectedDeco.Name,
Start = maybeStart,
End = maybeEnd,
LastStart = maybeStart,
LastEnd = maybeEnd
};
_decoTemplate.Items.Add(item);
break;
}
maybeStart = maybeStart.AddMinutes(5);
}
}
private async Task RemoveDecoFromDecoTemplate()
{
if (_decoToRemove is not null)
{
_decoTemplate.Items.Remove(_decoToRemove);
_decoToRemove = null;
await InvokeAsync(StateHasChanged);
}
}
private void CalendarItemChanged(CalendarItem calendarItem)
{
// don't allow any overlap
if (calendarItem is DecoTemplateItemEditViewModel item)
{
if (item.End.HasValue && IntersectsOthers(item, item.Start, item.End.Value))
{
// roll back
item.Start = item.LastStart;
item.End = item.LastEnd;
}
else
{
// commit
item.LastStart = item.Start;
item.LastEnd = item.End;
}
}
}
private bool IntersectsOthers(DecoTemplateItemEditViewModel item, DateTime start, DateTime end)
{
var willFit = true;
foreach (DecoTemplateItemEditViewModel existing in _decoTemplate.Items)
{
if (existing == item)
{
continue;
}
if (start < existing.End && existing.Start < end)
{
willFit = false;
break;
}
}
return willFit == false;
}
// private void RemoveDecoTemplateItem(DecoTemplateItemEditViewModel item)
// {
// _selectedItem = null;
// _decoTemplate.Items.Remove(item);
// }
private async Task SaveChanges()
{
await Task.Delay(10);
var items = _decoTemplate.Items.Map(item => new ReplaceDecoTemplateItem(item.DecoId, item.Start.TimeOfDay, item.End!.Value.TimeOfDay)).ToList();
Seq<BaseError> errorMessages = await Mediator.Send(new ReplaceDecoTemplateItems(Id, _decoTemplate.Name, items), _cts.Token)
.Map(e => e.LeftToSeq());
errorMessages.HeadOrNone().Match(
error =>
{
Snackbar.Add($"Unexpected error saving template: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving template: {Error}", error.Value);
},
() => NavigationManager.NavigateTo("/deco-templates"));
}
}

205
ErsatzTV/Pages/DecoTemplates.razor

@ -0,0 +1,205 @@ @@ -0,0 +1,205 @@
@page "/deco-templates"
@using S = System.Collections.Generic
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject ILogger<DecoTemplates> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Deco Templates</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3 mx-3" Label="Deco Template Group Name" @bind-Value="_decoTemplateGroupName" For="@(() => _decoTemplateGroupName)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplateGroup())" Class="ml-4 mb-4">
Add Deco Template Group
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardContent>
<div class="mx-4">
<MudSelect Label="Deco Template Group" @bind-Value="_selectedDecoTemplateGroup" Class="mt-3">
@foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups)
{
<MudSelectItem Value="@decoTemplateGroup">@decoTemplateGroup.Name</MudSelectItem>
}
</MudSelect>
<MudTextField Class="mt-3" Label="Deco Template Name" @bind-Value="_decoTemplateName" For="@(() => _decoTemplateName)"/>
</div>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoTemplate())" Class="ml-4 mb-4">
Add Deco Template
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCard>
<MudTreeView ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
<ItemTemplate Context="item">
<MudTreeViewItem Items="@item.TreeItems" Icon="@item.Icon" CanExpand="@item.CanExpand" Value="@item">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
<MudItem xs="8">
<MudText>@item.Text</MudText>
</MudItem>
</MudGrid>
<div style="justify-self: end;">
@foreach (int decoTemplateId in Optional(item.DecoTemplateId))
{
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Href="@($"deco-templates/{decoTemplateId}")"/>
}
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="@(_ => DeleteItem(item))"/>
</div>
</div>
</BodyContent>
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private S.HashSet<DecoTemplateTreeItemViewModel> TreeItems { get; set; } = [];
private List<DecoTemplateGroupViewModel> _decoTemplateGroups = [];
private DecoTemplateGroupViewModel _selectedDecoTemplateGroup;
private string _decoTemplateGroupName;
private string _decoTemplateName;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadDecoTemplateGroups();
await InvokeAsync(StateHasChanged);
}
private async Task ReloadDecoTemplateGroups()
{
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
TreeItems = _decoTemplateGroups.Map(g => new DecoTemplateTreeItemViewModel(g)).ToHashSet();
}
private async Task AddDecoTemplateGroup()
{
if (!string.IsNullOrWhiteSpace(_decoTemplateGroupName))
{
Either<BaseError, DecoTemplateGroupViewModel> result = await Mediator.Send(new CreateDecoTemplateGroup(_decoTemplateGroupName), _cts.Token);
foreach (BaseError error in result.LeftToSeq())
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error adding deco template group: {Error}", error.Value);
}
foreach (DecoTemplateGroupViewModel decoTemplateGroup in result.RightToSeq())
{
TreeItems.Add(new DecoTemplateTreeItemViewModel(decoTemplateGroup));
_decoTemplateGroupName = null;
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
await InvokeAsync(StateHasChanged);
}
}
}
private async Task AddDecoTemplate()
{
if (_selectedDecoTemplateGroup is not null && !string.IsNullOrWhiteSpace(_decoTemplateName))
{
Either<BaseError, DecoTemplateViewModel> result = await Mediator.Send(new CreateDecoTemplate(_selectedDecoTemplateGroup.Id, _decoTemplateName), _cts.Token);
foreach (BaseError error in result.LeftToSeq())
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error adding deco template: {Error}", error.Value);
}
foreach (DecoTemplateViewModel decoTemplate in result.RightToSeq())
{
foreach (DecoTemplateTreeItemViewModel item in TreeItems.Where(item => item.DecoTemplateGroupId == _selectedDecoTemplateGroup.Id))
{
item.TreeItems.Add(new DecoTemplateTreeItemViewModel(decoTemplate));
}
_decoTemplateName = null;
await InvokeAsync(StateHasChanged);
}
}
}
private async Task<S.HashSet<DecoTemplateTreeItemViewModel>> LoadServerData(DecoTemplateTreeItemViewModel parentNode)
{
foreach (int decoTemplateGroupId in Optional(parentNode.DecoTemplateGroupId))
{
List<DecoTemplateViewModel> result = await Mediator.Send(new GetDecoTemplatesByDecoTemplateGroupId(decoTemplateGroupId), _cts.Token);
foreach (DecoTemplateViewModel decoTemplate in result)
{
parentNode.TreeItems.Add(new DecoTemplateTreeItemViewModel(decoTemplate));
}
}
return parentNode.TreeItems;
}
private async Task DeleteItem(DecoTemplateTreeItemViewModel treeItem)
{
foreach (int decoTemplateGroupId in Optional(treeItem.DecoTemplateGroupId))
{
var parameters = new DialogParameters { { "EntityType", "Deco Template group" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco Template Group", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
{
await Mediator.Send(new DeleteDecoTemplateGroup(decoTemplateGroupId), _cts.Token);
TreeItems.RemoveWhere(i => i.DecoTemplateGroupId == decoTemplateGroupId);
_decoTemplateGroups = await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token);
await InvokeAsync(StateHasChanged);
}
}
foreach (int decoTemplateId in Optional(treeItem.DecoTemplateId))
{
var parameters = new DialogParameters { { "EntityType", "Deco Template" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco Template", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
{
await Mediator.Send(new DeleteDecoTemplate(decoTemplateId), _cts.Token);
foreach (DecoTemplateTreeItemViewModel parent in TreeItems)
{
parent.TreeItems.Remove(treeItem);
}
await InvokeAsync(StateHasChanged);
}
}
}
}

211
ErsatzTV/Pages/Decos.razor

@ -0,0 +1,211 @@ @@ -0,0 +1,211 @@
@page "/decos"
@using S = System.Collections.Generic
@using ErsatzTV.Application.Scheduling
@implements IDisposable
@inject ILogger<Decos> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
@inject IDialogService Dialog
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h4" Class="mb-4">Decos</MudText>
<MudGrid>
<MudItem xs="4">
<div style="max-width: 400px;" class="mr-4">
<MudCard>
<MudCardContent>
<MudTextField Class="mt-3 mx-3" Label="Deco Group Name" @bind-Value="_decoGroupName" For="@(() => _decoGroupName)"/>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDecoGroup())" Class="ml-4 mb-4">
Add Deco Group
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="4">
<div style="max-width: 400px;" class="mb-6">
<MudCard>
<MudCardContent>
<div class="mx-4">
<MudSelect Label="Deco Group" @bind-Value="_selectedDecoGroup" Class="mt-3">
@foreach (DecoGroupViewModel decoGroup in _decoGroups)
{
<MudSelectItem Value="@decoGroup">@decoGroup.Name</MudSelectItem>
}
</MudSelect>
<MudTextField Class="mt-3" Label="Deco Name" @bind-Value="_decoName" For="@(() => _decoName)"/>
</div>
</MudCardContent>
<MudCardActions>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(_ => AddDeco())" Class="ml-4 mb-4">
Add Deco
</MudButton>
</MudCardActions>
</MudCard>
</div>
</MudItem>
<MudItem xs="8">
<MudCard>
<MudTreeView ServerData="LoadServerData" Items="@TreeItems" Hover="true" ExpandOnClick="true">
<ItemTemplate Context="item">
<MudTreeViewItem Items="@item.TreeItems" Icon="@item.Icon" CanExpand="@item.CanExpand" Value="@item">
<BodyContent>
<div style="display: grid; grid-template-columns: 1fr auto; align-items: center; width: 100%">
<MudGrid Justify="Justify.FlexStart">
<MudItem xs="5">
<MudText>@item.Text</MudText>
</MudItem>
@if (!string.IsNullOrWhiteSpace(item.EndText))
{
<MudItem xs="6">
<MudText>@item.EndText</MudText>
</MudItem>
}
</MudGrid>
<div style="justify-self: end;">
@foreach (int decoId in Optional(item.DecoId))
{
<MudIconButton Icon="@Icons.Material.Filled.Edit" Size="Size.Medium" Color="Color.Inherit" Href="@($"decos/{decoId}")"/>
}
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Medium" Color="Color.Inherit" OnClick="@(_ => DeleteItem(item))"/>
</div>
</div>
</BodyContent>
</MudTreeViewItem>
</ItemTemplate>
</MudTreeView>
</MudCard>
</MudItem>
</MudGrid>
</MudContainer>
@code {
private readonly CancellationTokenSource _cts = new();
private S.HashSet<DecoTreeItemViewModel> TreeItems { get; set; } = [];
private List<DecoGroupViewModel> _decoGroups = [];
private DecoGroupViewModel _selectedDecoGroup;
private string _decoGroupName;
private string _decoName;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
await ReloadDecoGroups();
await InvokeAsync(StateHasChanged);
}
private async Task ReloadDecoGroups()
{
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
TreeItems = _decoGroups.Map(g => new DecoTreeItemViewModel(g)).ToHashSet();
}
private async Task AddDecoGroup()
{
if (!string.IsNullOrWhiteSpace(_decoGroupName))
{
Either<BaseError, DecoGroupViewModel> result = await Mediator.Send(new CreateDecoGroup(_decoGroupName), _cts.Token);
foreach (BaseError error in result.LeftToSeq())
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error adding deco group: {Error}", error.Value);
}
foreach (DecoGroupViewModel decoGroup in result.RightToSeq())
{
TreeItems.Add(new DecoTreeItemViewModel(decoGroup));
_decoGroupName = null;
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
await InvokeAsync(StateHasChanged);
}
}
}
private async Task AddDeco()
{
if (_selectedDecoGroup is not null && !string.IsNullOrWhiteSpace(_decoName))
{
Either<BaseError, DecoViewModel> result = await Mediator.Send(new CreateDeco(_selectedDecoGroup.Id, _decoName), _cts.Token);
foreach (BaseError error in result.LeftToSeq())
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error adding deco: {Error}", error.Value);
}
foreach (DecoViewModel deco in result.RightToSeq())
{
foreach (DecoTreeItemViewModel item in TreeItems.Where(item => item.DecoGroupId == _selectedDecoGroup.Id))
{
item.TreeItems.Add(new DecoTreeItemViewModel(deco));
}
_decoName = null;
await InvokeAsync(StateHasChanged);
}
}
}
private async Task<S.HashSet<DecoTreeItemViewModel>> LoadServerData(DecoTreeItemViewModel parentNode)
{
foreach (int decoGroupId in Optional(parentNode.DecoGroupId))
{
List<DecoViewModel> result = await Mediator.Send(new GetDecosByDecoGroupId(decoGroupId), _cts.Token);
foreach (DecoViewModel deco in result)
{
parentNode.TreeItems.Add(new DecoTreeItemViewModel(deco));
}
}
return parentNode.TreeItems;
}
private async Task DeleteItem(DecoTreeItemViewModel treeItem)
{
foreach (int decoGroupId in Optional(treeItem.DecoGroupId))
{
var parameters = new DialogParameters { { "EntityType", "deco group" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco Group", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
{
await Mediator.Send(new DeleteDecoGroup(decoGroupId), _cts.Token);
TreeItems.RemoveWhere(i => i.DecoGroupId == decoGroupId);
_decoGroups = await Mediator.Send(new GetAllDecoGroups(), _cts.Token);
await InvokeAsync(StateHasChanged);
}
}
foreach (int decoId in Optional(treeItem.DecoId))
{
var parameters = new DialogParameters { { "EntityType", "deco" }, { "EntityName", treeItem.Text } };
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall };
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Deco", parameters, options);
DialogResult result = await dialog.Result;
if (!result.Canceled)
{
await Mediator.Send(new DeleteDeco(decoId), _cts.Token);
foreach (DecoTreeItemViewModel parent in TreeItems)
{
parent.TreeItems.Remove(treeItem);
}
await InvokeAsync(StateHasChanged);
}
}
}
}

42
ErsatzTV/Pages/PlayoutTemplatesEditor.razor

@ -11,7 +11,7 @@ @@ -11,7 +11,7 @@
@inject IMediator Mediator
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudTable T="PlayoutTemplateEditViewModel" Hover="true" Items="_items.OrderBy(i => i.Index)" Dense="true" SelectedItem="@_selectedItem" SelectedItemChanged="@(vm => SelectedItemChanged(vm))">
<MudTable T="PlayoutTemplateEditViewModel" Class="mt-4" Hover="true" Items="_items.OrderBy(i => i.Index)" Dense="true" SelectedItem="@_selectedItem" SelectedItemChanged="@(vm => SelectedItemChanged(vm))">
<ToolBarContent>
<MudText Typo="Typo.h6">@_channelName Templates</MudText>
<MudSpacer/>
@ -109,6 +109,24 @@ @@ -109,6 +109,24 @@
</MudSelect>
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardContent>
<MudSelect T="DecoTemplateGroupViewModel" Label="Deco Template Group" Value="@_selectedDecoTemplateGroup" ValueChanged="@(vm => UpdateDecoTemplateGroupItems(vm))">
@foreach (DecoTemplateGroupViewModel decoTemplateGroup in _decoTemplateGroups)
{
<MudSelectItem Value="@decoTemplateGroup">
@decoTemplateGroup.Name
</MudSelectItem>
}
</MudSelect>
<MudSelect Label="Deco Template" @bind-Value="_selectedItem.DecoTemplate" For="@(() => _selectedItem.DecoTemplate)" Clearable="true">
@foreach (DecoTemplateViewModel decoTemplate in _decoTemplates)
{
<MudSelectItem Value="@decoTemplate">@decoTemplate.Name</MudSelectItem>
}
</MudSelect>
</MudCardContent>
</MudCard>
<MudCard Class="mt-4">
<MudCardContent>
<MudCheckBox T="bool" Class="mt-3" Label="Limit To Date Range"
@ -285,9 +303,13 @@ @@ -285,9 +303,13 @@
private readonly List<TemplateGroupViewModel> _templateGroups = [];
private readonly List<TemplateViewModel> _templates = [];
private readonly List<DecoTemplateGroupViewModel> _decoTemplateGroups = [];
private readonly List<DecoTemplateViewModel> _decoTemplates = [];
private string _channelName;
private List<PlayoutTemplateEditViewModel> _items = [];
private TemplateGroupViewModel _selectedGroup;
private DecoTemplateGroupViewModel _selectedDecoTemplateGroup;
private PlayoutTemplateEditViewModel _selectedItem;
private readonly List<CalendarItem> _previewItems = [];
@ -306,6 +328,9 @@ @@ -306,6 +328,9 @@
_templateGroups.Clear();
_templateGroups.AddRange(await Mediator.Send(new GetAllTemplateGroups(), _cts.Token));
_decoTemplateGroups.Clear();
_decoTemplateGroups.AddRange(await Mediator.Send(new GetAllDecoTemplateGroups(), _cts.Token));
List<PlayoutTemplateViewModel> results = await Mediator.Send(new GetPlayoutTemplates(Id), _cts.Token);
_items = results.Map(ProjectToEditViewModel).ToList();
if (_items.Count == 1)
@ -319,6 +344,7 @@ @@ -319,6 +344,7 @@
{
Id = item.Id,
Template = item.Template,
DecoTemplate = item.DecoTemplate,
Index = item.Index,
DaysOfWeek = item.DaysOfWeek.ToList(),
DaysOfMonth = item.DaysOfMonth.ToList(),
@ -338,6 +364,14 @@ @@ -338,6 +364,14 @@
_templates.AddRange(await Mediator.Send(new GetTemplatesByTemplateGroupId(_selectedGroup.Id), _cts.Token));
}
private async Task UpdateDecoTemplateGroupItems(DecoTemplateGroupViewModel decoTemplateGroup)
{
_selectedDecoTemplateGroup = decoTemplateGroup;
_decoTemplates.Clear();
_decoTemplates.AddRange(await Mediator.Send(new GetDecoTemplatesByDecoTemplateGroupId(_selectedDecoTemplateGroup.Id), _cts.Token));
}
private async Task SelectedItemChanged(PlayoutTemplateEditViewModel template)
{
_selectedItem = template;
@ -346,6 +380,11 @@ @@ -346,6 +380,11 @@
{
await UpdateTemplateGroupItems(group);
}
foreach (DecoTemplateGroupViewModel group in Optional(_decoTemplateGroups.Find(group => group.Id == _selectedItem.DecoTemplate?.DecoTemplateGroupId)))
{
await UpdateDecoTemplateGroupItems(group);
}
}
private void DayOfWeekChanged(DayOfWeek dayOfWeek, bool isChecked)
@ -486,6 +525,7 @@ @@ -486,6 +525,7 @@
item.Id,
item.Index,
item.Template.Id,
item.DecoTemplate?.Id,
item.DaysOfWeek,
item.DaysOfMonth,
item.MonthsOfYear,

20
ErsatzTV/Pages/Playouts.razor

@ -82,8 +82,6 @@ @@ -82,8 +82,6 @@
</div>
@if (context.PlayoutType == ProgramSchedulePlayoutType.Flood)
{
<div style="width: 48px"></div>
<div style="width: 48px"></div>
<MudTooltip Text="Edit Alternate Schedules">
<MudIconButton Icon="@Icons.Material.Filled.EditCalendar"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
@ -105,8 +103,6 @@ @@ -105,8 +103,6 @@
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.ExternalJson)
{
<div style="width: 48px"></div>
<div style="width: 48px"></div>
<MudTooltip Text="Edit External Json File">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
@ -118,22 +114,10 @@ @@ -118,22 +114,10 @@
}
else if (context.PlayoutType == ProgramSchedulePlayoutType.Block)
{
<MudTooltip Text="Erase Items">
<MudIconButton Icon="@Icons.Material.Filled.Clear"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => EraseItems(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Erase Items and History">
<MudIconButton Icon="@Icons.Material.Filled.ClearAll"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
OnClick="@(_ => EraseHistory(context))">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Edit Templates">
<MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
Link="@($"playouts/{context.PlayoutId}/templates")">
Link="@($"playouts/block/{context.PlayoutId}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Reset Playout">

10
ErsatzTV/Shared/MainLayout.razor

@ -100,13 +100,13 @@ @@ -100,13 +100,13 @@
<MudNavLink Href="channels">Channels</MudNavLink>
<MudNavLink Href="ffmpeg">FFmpeg Profiles</MudNavLink>
<MudNavLink Href="watermarks">Watermarks</MudNavLink>
<MudNavGroup Title="Media Sources" Expanded="true">
<MudNavGroup Title="Media Sources">
<MudNavLink Href="media/sources/local">Local</MudNavLink>
<MudNavLink Href="media/sources/emby">Emby</MudNavLink>
<MudNavLink Href="media/sources/jellyfin">Jellyfin</MudNavLink>
<MudNavLink Href="media/sources/plex">Plex</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Media" Expanded="true">
<MudNavGroup Title="Media">
<MudNavLink Href="media/libraries">Libraries</MudNavLink>
<MudNavLink Href="media/trash">Trash</MudNavLink>
<MudNavLink Href="media/tv/shows">TV Shows</MudNavLink>
@ -116,15 +116,17 @@ @@ -116,15 +116,17 @@
<MudNavLink Href="media/music/songs">Songs</MudNavLink>
<MudNavLink Href="media/browser/images">Images</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Lists" Expanded="true">
<MudNavGroup Title="Lists">
<MudNavLink Href="media/collections">Collections</MudNavLink>
<MudNavLink Href="media/trakt/lists">Trakt Lists</MudNavLink>
<MudNavLink Href="media/filler/presets">Filler Presets</MudNavLink>
</MudNavGroup>
<MudNavGroup Title="Scheduling" Expanded="true">
<MudNavGroup Title="Scheduling">
<MudNavLink Href="schedules">Schedules</MudNavLink>
<MudNavLink Href="blocks">Blocks</MudNavLink>
<MudNavLink Href="templates">Templates</MudNavLink>
<MudNavLink Href="decos">Decos</MudNavLink>
<MudNavLink Href="deco-templates">Deco Templates</MudNavLink>
<MudNavLink Href="playouts">Playouts</MudNavLink>
</MudNavGroup>
<MudNavLink Href="settings">Settings</MudNavLink>

8
ErsatzTV/ViewModels/DecoEditViewModel.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.ViewModels;
public class DecoEditViewModel
{
public int DecoGroupId { get; set; }
public string Name { get; set; }
public int? WatermarkId { get; set; }
}

22
ErsatzTV/ViewModels/DecoTemplateItemEditViewModel.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using Heron.MudCalendar;
namespace ErsatzTV.ViewModels;
public class DecoTemplateItemEditViewModel : CalendarItem
{
private string _blockName;
public int DecoId { get; set; }
public string DecoName
{
get => _blockName;
set
{
_blockName = value;
Text = value;
}
}
public DateTime LastStart { get; set; }
public DateTime? LastEnd { get; set; }
}

7
ErsatzTV/ViewModels/DecoTemplateItemsEditViewModel.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.ViewModels;
public class DecoTemplateItemsEditViewModel
{
public string Name { get; set; }
public List<DecoTemplateItemEditViewModel> Items { get; set; }
}

37
ErsatzTV/ViewModels/DecoTemplateTreeItemViewModel.cs

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
using ErsatzTV.Application.Scheduling;
using MudBlazor;
using S = System.Collections.Generic;
namespace ErsatzTV.ViewModels;
public class DecoTemplateTreeItemViewModel
{
public DecoTemplateTreeItemViewModel(DecoTemplateGroupViewModel decoTemplateGroup)
{
Text = decoTemplateGroup.Name;
TreeItems = [];
CanExpand = decoTemplateGroup.DecoTemplateCount > 0;
DecoTemplateGroupId = decoTemplateGroup.Id;
Icon = Icons.Material.Filled.Folder;
}
public DecoTemplateTreeItemViewModel(DecoTemplateViewModel decoTemplate)
{
Text = decoTemplate.Name;
TreeItems = [];
CanExpand = false;
DecoTemplateId = decoTemplate.Id;
}
public string Text { get; }
public string Icon { get; }
public bool CanExpand { get; }
public int? DecoTemplateId { get; }
public int? DecoTemplateGroupId { get; }
public S.HashSet<DecoTemplateTreeItemViewModel> TreeItems { get; }
}

40
ErsatzTV/ViewModels/DecoTreeItemViewModel.cs

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
using ErsatzTV.Application.Scheduling;
using MudBlazor;
using S = System.Collections.Generic;
namespace ErsatzTV.ViewModels;
public class DecoTreeItemViewModel
{
public DecoTreeItemViewModel(DecoGroupViewModel decoGroup)
{
Text = decoGroup.Name;
EndText = string.Empty;
TreeItems = [];
CanExpand = decoGroup.DecoCount > 0;
DecoGroupId = decoGroup.Id;
Icon = Icons.Material.Filled.Folder;
}
public DecoTreeItemViewModel(DecoViewModel deco)
{
Text = deco.Name;
TreeItems = [];
CanExpand = false;
DecoId = deco.Id;
}
public string Text { get; }
public string EndText { get; }
public string Icon { get; }
public bool CanExpand { get; }
public int? DecoId { get; }
public int? DecoGroupId { get; }
public S.HashSet<DecoTreeItemViewModel> TreeItems { get; }
}

3
ErsatzTV/ViewModels/PlayoutTemplateEditViewModel.cs

@ -1,3 +1,4 @@ @@ -1,3 +1,4 @@
using ErsatzTV.Annotations;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Scheduling;
@ -13,6 +14,8 @@ public class PlayoutTemplateEditViewModel @@ -13,6 +14,8 @@ public class PlayoutTemplateEditViewModel
public int Id { get; set; }
public int Index { get; set; }
public TemplateViewModel Template { get; set; }
[CanBeNull]
public DecoTemplateViewModel DecoTemplate { get; set; }
public List<DayOfWeek> DaysOfWeek { get; set; }
public List<int> DaysOfMonth { get; set; }
public List<int> MonthsOfYear { get; set; }

Loading…
Cancel
Save