Browse Source

first pass at block scheduling (#1548)

* add blocks, block groups

* basic block and block item editing

* add template groups and basic template editing (name)

* add blocks to template calendar

* edit playout templates

* add calendar preview to playout templates

* add basic block playout building

* add mysql migration

* update changelog
pull/1551/head
Jason Dove 2 years ago committed by GitHub
parent
commit
dcbe4837bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      CHANGELOG.md
  2. 1
      Directory.Build.props
  3. 2
      ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings
  4. 25
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  5. 67
      ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs
  6. 3
      ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs
  7. 4
      ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs
  8. 2
      ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs
  9. 3
      ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs
  10. 15
      ErsatzTV.Application/Scheduling/BlockItemViewModel.cs
  11. 3
      ErsatzTV.Application/Scheduling/BlockViewModel.cs
  12. 5
      ErsatzTV.Application/Scheduling/Commands/CreateBlock.cs
  13. 5
      ErsatzTV.Application/Scheduling/Commands/CreateBlockGroup.cs
  14. 31
      ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs
  15. 40
      ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs
  16. 5
      ErsatzTV.Application/Scheduling/Commands/CreateTemplate.cs
  17. 5
      ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroup.cs
  18. 35
      ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.cs
  19. 39
      ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.cs
  20. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteBlock.cs
  21. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroup.cs
  22. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroupHandler.cs
  23. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteBlockHandler.cs
  24. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteTemplate.cs
  25. 5
      ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroup.cs
  26. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroupHandler.cs
  27. 29
      ErsatzTV.Application/Scheduling/Commands/DeleteTemplateHandler.cs
  28. 12
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItem.cs
  29. 6
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItems.cs
  30. 123
      ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs
  31. 9
      ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplate.cs
  32. 6
      ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItems.cs
  33. 82
      ErsatzTV.Application/Scheduling/Commands/ReplacePlayoutTemplateItemsHandler.cs
  34. 3
      ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItem.cs
  35. 6
      ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItems.cs
  36. 64
      ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItemsHandler.cs
  37. 56
      ErsatzTV.Application/Scheduling/Mapper.cs
  38. 9
      ErsatzTV.Application/Scheduling/PlayoutTemplateViewModel.cs
  39. 3
      ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroups.cs
  40. 21
      ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.cs
  41. 3
      ErsatzTV.Application/Scheduling/Queries/GetAllTemplateGroups.cs
  42. 23
      ErsatzTV.Application/Scheduling/Queries/GetAllTemplateGroupsHandler.cs
  43. 3
      ErsatzTV.Application/Scheduling/Queries/GetBlockById.cs
  44. 17
      ErsatzTV.Application/Scheduling/Queries/GetBlockByIdHandler.cs
  45. 3
      ErsatzTV.Application/Scheduling/Queries/GetBlockItems.cs
  46. 36
      ErsatzTV.Application/Scheduling/Queries/GetBlockItemsHandler.cs
  47. 3
      ErsatzTV.Application/Scheduling/Queries/GetBlocksByBlockGroupId.cs
  48. 21
      ErsatzTV.Application/Scheduling/Queries/GetBlocksByBlockGroupIdHandler.cs
  49. 3
      ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplates.cs
  50. 24
      ErsatzTV.Application/Scheduling/Queries/GetPlayoutTemplatesHandler.cs
  51. 3
      ErsatzTV.Application/Scheduling/Queries/GetTemplateById.cs
  52. 17
      ErsatzTV.Application/Scheduling/Queries/GetTemplateByIdHandler.cs
  53. 3
      ErsatzTV.Application/Scheduling/Queries/GetTemplateItems.cs
  54. 19
      ErsatzTV.Application/Scheduling/Queries/GetTemplateItemsHandler.cs
  55. 3
      ErsatzTV.Application/Scheduling/Queries/GetTemplatesByTemplateGroupId.cs
  56. 21
      ErsatzTV.Application/Scheduling/Queries/GetTemplatesByTemplateGroupIdHandler.cs
  57. 3
      ErsatzTV.Application/Scheduling/TemplateGroupViewModel.cs
  58. 3
      ErsatzTV.Application/Scheduling/TemplateItemViewModel.cs
  59. 3
      ErsatzTV.Application/Scheduling/TemplateViewModel.cs
  60. 6
      ErsatzTV.Core/Domain/Playout.cs
  61. 1
      ErsatzTV.Core/Domain/ProgramSchedulePlayoutType.cs
  62. 13
      ErsatzTV.Core/Domain/Scheduling/Block.cs
  63. 8
      ErsatzTV.Core/Domain/Scheduling/BlockGroup.cs
  64. 19
      ErsatzTV.Core/Domain/Scheduling/BlockItem.cs
  65. 21
      ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs
  66. 32
      ErsatzTV.Core/Domain/Scheduling/PlayoutTemplate.cs
  67. 11
      ErsatzTV.Core/Domain/Scheduling/Template.cs
  68. 8
      ErsatzTV.Core/Domain/Scheduling/TemplateGroup.cs
  69. 11
      ErsatzTV.Core/Domain/Scheduling/TemplateItem.cs
  70. 9
      ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs
  71. 9
      ErsatzTV.Core/Interfaces/Scheduling/IExternalJsonPlayoutBuilder.cs
  72. 367
      ErsatzTV.Core/Scheduling/BlockPlayoutBuilder.cs
  73. 41
      ErsatzTV.Core/Scheduling/CollectionKey.cs
  74. 19
      ErsatzTV.Core/Scheduling/ExternalJsonPlayoutBuilder.cs
  75. 7
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  76. 34
      ErsatzTV.Core/Scheduling/PlayoutTemplateSelector.cs
  77. 2
      ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj
  78. 4895
      ErsatzTV.Infrastructure.MySql/Migrations/20240114034944_Add_BlockScheduling.Designer.cs
  79. 355
      ErsatzTV.Infrastructure.MySql/Migrations/20240114034944_Add_BlockScheduling.cs
  80. 369
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  81. 6
      ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj
  82. 4673
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240110162335_Add_Block.Designer.cs
  83. 153
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240110162335_Add_Block.cs
  84. 4789
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113002246_Add_Template.Designer.cs
  85. 126
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113002246_Add_Template.cs
  86. 4837
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113121929_Add_PlayoutTemplate.Designer.cs
  87. 93
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113121929_Add_PlayoutTemplate.cs
  88. 4840
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113140741_Add_PlayoutTemplate_DaysOfMonth.Designer.cs
  89. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113140741_Add_PlayoutTemplate_DaysOfMonth.cs
  90. 4893
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113190523_Add_PlayoutHistory.Designer.cs
  91. 61
      ErsatzTV.Infrastructure.Sqlite/Migrations/20240113190523_Add_PlayoutHistory.cs
  92. 393
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  93. 10
      ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs
  94. 31
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockConfiguration.cs
  95. 21
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockGroupConfiguration.cs
  96. 37
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockItemConfiguration.cs
  97. 13
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/PlayoutHistoryConfiguration.cs
  98. 22
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/PlayoutTemplateConfiguration.cs
  99. 26
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateConfiguration.cs
  100. 21
      ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateGroupConfiguration.cs
  101. Some files were not shown because too many files have changed in this diff Show More

8
CHANGELOG.md

@ -20,6 +20,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -20,6 +20,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- If found, ErsatzTV will run ffprobe to get statistics immediately before streaming from disk
- When local files are unavailable, ErsatzTV must be logged into the same Plex server as DizqueTV
- ErsatzTV will ask Plex for statistics immediately before streaming from Plex
- Add new *experimental* playout type `Block`
- **This playout type is under active development and updates may reset or delete related playout data**
- Many planned features are missing, incomplete, or result in errors. This is expected.
- Block playouts consist of:
- `Blocks` - ordered list of items to play within the specified duration
- `Templates` - a generic "day" that consists of blocks scheduled at specific times
- `Playout Templates` - templates to schedule using the specified criteria. Only one template will be selected each day
- Much more to come on this feature as development continues
### Fixed
- Fix error loading path replacements when using MySql

1
Directory.Build.props

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
<Project>
<PropertyGroup>
<InformationalVersion>develop</InformationalVersion>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
</Project>

2
ErsatzTV.Application/ErsatzTV.Application.csproj.DotSettings

@ -35,6 +35,8 @@ @@ -35,6 +35,8 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=programschedules_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=resolutions_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=scheduling_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=scheduling_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Ccommands/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=search_005Cqueries/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=streaming_005Ccommands/@EntryIndexedValue">True</s:Boolean>

25
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -22,12 +22,16 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -22,12 +22,16 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private readonly IEntityLocker _entityLocker;
private readonly IFFmpegSegmenterService _ffmpegSegmenterService;
private readonly IPlayoutBuilder _playoutBuilder;
private readonly IBlockPlayoutBuilder _blockPlayoutBuilder;
private readonly IExternalJsonPlayoutBuilder _externalJsonPlayoutBuilder;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public BuildPlayoutHandler(
IClient client,
IDbContextFactory<TvContext> dbContextFactory,
IPlayoutBuilder playoutBuilder,
IBlockPlayoutBuilder blockPlayoutBuilder,
IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder,
IFFmpegSegmenterService ffmpegSegmenterService,
IEntityLocker entityLocker,
ChannelWriter<IBackgroundServiceRequest> workerChannel)
@ -35,6 +39,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -35,6 +39,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
_client = client;
_dbContextFactory = dbContextFactory;
_playoutBuilder = playoutBuilder;
_blockPlayoutBuilder = blockPlayoutBuilder;
_externalJsonPlayoutBuilder = externalJsonPlayoutBuilder;
_ffmpegSegmenterService = ffmpegSegmenterService;
_entityLocker = entityLocker;
_workerChannel = workerChannel;
@ -59,7 +65,20 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -59,7 +65,20 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
{
_entityLocker.LockPlayout(playout.Id);
switch (playout.ProgramSchedulePlayoutType)
{
case ProgramSchedulePlayoutType.Block:
await _blockPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
break;
case ProgramSchedulePlayoutType.ExternalJson:
await _externalJsonPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
break;
case ProgramSchedulePlayoutType.None:
case ProgramSchedulePlayoutType.Flood:
default:
await _playoutBuilder.Build(playout, request.Mode, cancellationToken);
break;
}
// let any active segmenter processes know that the playout has been modified
// and therefore the segmenter may need to seek into the next item instead of
@ -135,6 +154,12 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -135,6 +154,12 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.PlayoutHistory)
.Include(p => p.Templates)
.ThenInclude(t => t.Template)
.ThenInclude(t => t.Items)
.ThenInclude(i => i.Block)
.ThenInclude(b => b.Items)
.Include(p => p.FillGroupIndices)
.ThenInclude(fgi => fgi.EnumeratorState)
.Include(p => p.ProgramScheduleAlternates)

67
ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
using System.Threading.Channels;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts;
public class CreateBlockPlayoutHandler(
ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateBlockPlayout, Either<BaseError, CreatePlayoutResponse>>
{
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
CreateBlockPlayout request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Playout> validation = await Validate(dbContext, request);
return await validation.Apply(playout => PersistPlayout(dbContext, playout));
}
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout)
{
await dbContext.Playouts.AddAsync(playout);
await dbContext.SaveChangesAsync();
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset));
await channel.WriteAsync(new RefreshChannelList());
return new CreatePlayoutResponse(playout.Id);
}
private static async Task<Validation<BaseError, Playout>> Validate(
TvContext dbContext,
CreateBlockPlayout request) =>
(await ValidateChannel(dbContext, request), ValidatePlayoutType(request))
.Apply(
(channel, playoutType) => new Playout
{
ChannelId = channel.Id,
ProgramSchedulePlayoutType = playoutType
});
private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext,
CreateBlockPlayout createBlockPlayout) =>
dbContext.Channels
.Include(c => c.Playouts)
.SelectOneAsync(c => c.Id, c => c.Id == createBlockPlayout.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist"))
.BindT(ChannelMustNotHavePlayouts);
private static Validation<BaseError, Channel> ChannelMustNotHavePlayouts(Channel channel) =>
Optional(channel.Playouts.Count)
.Filter(count => count == 0)
.Map(_ => channel)
.ToValidation<BaseError>("Channel already has one playout");
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType(
CreateBlockPlayout createBlockPlayout) =>
Optional(createBlockPlayout.ProgramSchedulePlayoutType)
.Filter(playoutType => playoutType == ProgramSchedulePlayoutType.Block)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must be Block");
}

3
ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs

@ -9,5 +9,8 @@ public record CreatePlayout(int ChannelId, ProgramSchedulePlayoutType ProgramSch @@ -9,5 +9,8 @@ public record CreatePlayout(int ChannelId, ProgramSchedulePlayoutType ProgramSch
public record CreateFloodPlayout(int ChannelId, int ProgramScheduleId)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Flood);
public record CreateBlockPlayout(int ChannelId)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Block);
public record CreateExternalJsonPlayout(int ChannelId, string ExternalJsonFile)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.ExternalJson);

4
ErsatzTV.Application/Playouts/Commands/DeletePlayoutHandler.cs

@ -25,9 +25,7 @@ public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseEr @@ -25,9 +25,7 @@ public class DeletePlayoutHandler : IRequestHandler<DeletePlayout, Either<BaseEr
_localFileSystem = localFileSystem;
}
public async Task<Either<BaseError, Unit>> Handle(
DeletePlayout request,
CancellationToken cancellationToken)
public async Task<Either<BaseError, Unit>> Handle(DeletePlayout request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);

2
ErsatzTV.Application/ProgramSchedules/Queries/GetProgramScheduleItemsHandler.cs

@ -56,7 +56,7 @@ public class GetProgramScheduleItemsHandler : @@ -56,7 +56,7 @@ public class GetProgramScheduleItemsHandler :
.Map(psi => EnforceProperties(maybeProgramSchedule, psi)).ToList());
}
// shuffled schedule items supports a limited set of properly values
// shuffled schedule items supports a limited set of property values
private static ProgramScheduleItemViewModel EnforceProperties(
Option<ProgramSchedule> maybeProgramSchedule,
ProgramScheduleItemViewModel item)

3
ErsatzTV.Application/Scheduling/BlockGroupViewModel.cs

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

15
ErsatzTV.Application/Scheduling/BlockItemViewModel.cs

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Scheduling;
public record BlockItemViewModel(
int Id,
int Index,
ProgramScheduleItemCollectionType CollectionType,
MediaCollectionViewModel Collection,
MultiCollectionViewModel MultiCollection,
SmartCollectionViewModel SmartCollection,
NamedMediaItemViewModel MediaItem,
PlaybackOrder PlaybackOrder);

3
ErsatzTV.Application/Scheduling/BlockViewModel.cs

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

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

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

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

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

31
ErsatzTV.Application/Scheduling/Commands/CreateBlockGroupHandler.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class CreateBlockGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateBlockGroup, Either<BaseError, BlockGroupViewModel>>
{
public async Task<Either<BaseError, BlockGroupViewModel>> Handle(CreateBlockGroup request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, BlockGroup> validation = await Validate(request);
return await validation.Apply(profile => PersistBlockGroup(dbContext, profile));
}
private static async Task<BlockGroupViewModel> PersistBlockGroup(TvContext dbContext, BlockGroup blockGroup)
{
await dbContext.BlockGroups.AddAsync(blockGroup);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(blockGroup);
}
private static Task<Validation<BaseError, BlockGroup>> Validate(CreateBlockGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new BlockGroup { Name = name, Blocks = [] }));
private static Validation<BaseError, string> ValidateName(CreateBlockGroup createBlockGroup) =>
createBlockGroup.NotEmpty(x => x.Name)
.Bind(_ => createBlockGroup.NotLongerThan(50)(x => x.Name));
}

40
ErsatzTV.Application/Scheduling/Commands/CreateBlockHandler.cs

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class CreateBlockHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateBlock, Either<BaseError, BlockViewModel>>
{
public async Task<Either<BaseError, BlockViewModel>> Handle(
CreateBlock request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Block> validation = await Validate(request);
return await validation.Apply(profile => PersistBlock(dbContext, profile));
}
private static async Task<BlockViewModel> PersistBlock(TvContext dbContext, Block block)
{
await dbContext.Blocks.AddAsync(block);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(block);
}
private static Task<Validation<BaseError, Block>> Validate(CreateBlock request) =>
Task.FromResult(
ValidateName(request).Map(
name => new Block
{
BlockGroupId = request.BlockGroupId,
Name = name,
Minutes = 30
}));
private static Validation<BaseError, string> ValidateName(CreateBlock createBlock) =>
createBlock.NotEmpty(x => x.Name)
.Bind(_ => createBlock.NotLongerThan(50)(x => x.Name));
}

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

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

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

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

35
ErsatzTV.Application/Scheduling/Commands/CreateTemplateGroupHandler.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 CreateTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateTemplateGroup, Either<BaseError, TemplateGroupViewModel>>
{
public async Task<Either<BaseError, TemplateGroupViewModel>> Handle(
CreateTemplateGroup request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, TemplateGroup> validation = await Validate(request);
return await validation.Apply(profile => PersistTemplateGroup(dbContext, profile));
}
private static async Task<TemplateGroupViewModel> PersistTemplateGroup(
TvContext dbContext,
TemplateGroup templateGroup)
{
await dbContext.TemplateGroups.AddAsync(templateGroup);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(templateGroup);
}
private static Task<Validation<BaseError, TemplateGroup>> Validate(CreateTemplateGroup request) =>
Task.FromResult(ValidateName(request).Map(name => new TemplateGroup { Name = name, Templates = [] }));
private static Validation<BaseError, string> ValidateName(CreateTemplateGroup createTemplateGroup) =>
createTemplateGroup.NotEmpty(x => x.Name)
.Bind(_ => createTemplateGroup.NotLongerThan(50)(x => x.Name));
}

39
ErsatzTV.Application/Scheduling/Commands/CreateTemplateHandler.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 CreateTemplateHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<CreateTemplate, Either<BaseError, TemplateViewModel>>
{
public async Task<Either<BaseError, TemplateViewModel>> Handle(
CreateTemplate request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Template> validation = await Validate(request);
return await validation.Apply(profile => PersistTemplate(dbContext, profile));
}
private static async Task<TemplateViewModel> PersistTemplate(TvContext dbContext, Template template)
{
await dbContext.Templates.AddAsync(template);
await dbContext.SaveChangesAsync();
return Mapper.ProjectToViewModel(template);
}
private static Task<Validation<BaseError, Template>> Validate(CreateTemplate request) =>
Task.FromResult(
ValidateName(request).Map(
name => new Template
{
TemplateGroupId = request.TemplateGroupId,
Name = name
}));
private static Validation<BaseError, string> ValidateName(CreateTemplate createTemplate) =>
createTemplate.NotEmpty(x => x.Name)
.Bind(_ => createTemplate.NotLongerThan(50)(x => x.Name));
}

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

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

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

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

29
ErsatzTV.Application/Scheduling/Commands/DeleteBlockGroupHandler.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 DeleteBlockGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteBlockGroup, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteBlockGroup request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<BlockGroup> maybeBlockGroup = await dbContext.BlockGroups
.SelectOneAsync(p => p.Id, p => p.Id == request.BlockGroupId);
foreach (BlockGroup blockGroup in maybeBlockGroup)
{
dbContext.BlockGroups.Remove(blockGroup);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeBlockGroup.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"BlockGroup {request.BlockGroupId} does not exist."));
}
}

29
ErsatzTV.Application/Scheduling/Commands/DeleteBlockHandler.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 DeleteBlockHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteBlock, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteBlock request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Block> maybeBlock = await dbContext.Blocks
.SelectOneAsync(p => p.Id, p => p.Id == request.BlockId);
foreach (Block block in maybeBlock)
{
dbContext.Blocks.Remove(block);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeBlock.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"Block {request.BlockId} does not exist."));
}
}

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

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

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

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

29
ErsatzTV.Application/Scheduling/Commands/DeleteTemplateGroupHandler.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 DeleteTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteTemplateGroup, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteTemplateGroup request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<TemplateGroup> maybeTemplateGroup = await dbContext.TemplateGroups
.SelectOneAsync(p => p.Id, p => p.Id == request.TemplateGroupId);
foreach (TemplateGroup templateGroup in maybeTemplateGroup)
{
dbContext.TemplateGroups.Remove(templateGroup);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeTemplateGroup.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"TemplateGroup {request.TemplateGroupId} does not exist."));
}
}

29
ErsatzTV.Application/Scheduling/Commands/DeleteTemplateHandler.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 DeleteTemplateHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<DeleteTemplate, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(DeleteTemplate request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Template> maybeTemplate = await dbContext.Templates
.SelectOneAsync(p => p.Id, p => p.Id == request.TemplateId);
foreach (Template template in maybeTemplate)
{
dbContext.Templates.Remove(template);
await dbContext.SaveChangesAsync(cancellationToken);
}
return maybeTemplate.Match(
_ => Option<BaseError>.None,
() => BaseError.New($"Template {request.TemplateId} does not exist."));
}
}

12
ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItem.cs

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Scheduling;
public record ReplaceBlockItem(
int Index,
ProgramScheduleItemCollectionType CollectionType,
int? CollectionId,
int? MultiCollectionId,
int? SmartCollectionId,
int? MediaItemId,
PlaybackOrder PlaybackOrder);

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

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record ReplaceBlockItems(int BlockId, string Name, int Minutes, List<ReplaceBlockItem> Items)
: IRequest<Either<BaseError, List<BlockItemViewModel>>>;

123
ErsatzTV.Application/Scheduling/Commands/ReplaceBlockItemsHandler.cs

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<ReplaceBlockItems, Either<BaseError, List<BlockItemViewModel>>>
{
public async Task<Either<BaseError, List<BlockItemViewModel>>> Handle(
ReplaceBlockItems request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Block> validation = await Validate(dbContext, request);
return await validation.Apply(ps => Persist(dbContext, request, ps));
}
private static async Task<List<BlockItemViewModel>> Persist(
TvContext dbContext,
ReplaceBlockItems request,
Block block)
{
block.Name = request.Name;
block.Minutes = request.Minutes;
dbContext.RemoveRange(block.Items);
block.Items = request.Items.Map(i => BuildItem(block, i.Index, 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));
// }
return block.Items.Map(Mapper.ProjectToViewModel).ToList();
}
private static BlockItem BuildItem(Block block, int index, ReplaceBlockItem item) =>
new()
{
BlockId = block.Id,
Index = index,
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
MultiCollectionId = item.MultiCollectionId,
SmartCollectionId = item.SmartCollectionId,
MediaItemId = item.MediaItemId,
PlaybackOrder = item.PlaybackOrder
};
private static Task<Validation<BaseError, Block>> Validate(TvContext dbContext, ReplaceBlockItems request) =>
BlockMustExist(dbContext, request.BlockId)
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule));
private static Task<Validation<BaseError, Block>> BlockMustExist(TvContext dbContext, int blockId) =>
dbContext.Blocks
.Include(b => b.Items)
.SelectOneAsync(b => b.Id, b => b.Id == blockId)
.Map(o => o.ToValidation<BaseError>("[BlockId] does not exist."));
private static Validation<BaseError, Block> CollectionTypesMustBeValid(ReplaceBlockItems request, Block block) =>
request.Items.Map(item => CollectionTypeMustBeValid(item, block)).Sequence().Map(_ => block);
private static Validation<BaseError, Block> CollectionTypeMustBeValid(ReplaceBlockItem item, Block block)
{
switch (item.CollectionType)
{
case ProgramScheduleItemCollectionType.Collection:
if (item.CollectionId is null)
{
return BaseError.New("[Collection] is required for collection type 'Collection'");
}
break;
case ProgramScheduleItemCollectionType.TelevisionShow:
if (item.MediaItemId is null)
{
return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'");
}
break;
case ProgramScheduleItemCollectionType.TelevisionSeason:
if (item.MediaItemId is null)
{
return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'");
}
break;
case ProgramScheduleItemCollectionType.Artist:
if (item.MediaItemId is null)
{
return BaseError.New("[MediaItem] is required for collection type 'Artist'");
}
break;
case ProgramScheduleItemCollectionType.MultiCollection:
if (item.MultiCollectionId is null)
{
return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'");
}
break;
case ProgramScheduleItemCollectionType.SmartCollection:
if (item.SmartCollectionId is null)
{
return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'");
}
break;
case ProgramScheduleItemCollectionType.FakeCollection:
default:
return BaseError.New("[CollectionType] is invalid");
}
return block;
}
}

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

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Application.Scheduling;
public record ReplacePlayoutTemplate(
int Id,
int Index,
int TemplateId,
List<DayOfWeek> DaysOfWeek,
List<int> DaysOfMonth,
List<int> MonthsOfYear);

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

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record ReplacePlayoutTemplateItems(int PlayoutId, List<ReplacePlayoutTemplate> Items)
: IRequest<Option<BaseError>>;

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

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Application.Scheduling;
public class ReplacePlayoutTemplateItemsHandler(
IDbContextFactory<TvContext> dbContextFactory,
ILogger<ReplacePlayoutTemplateItemsHandler> logger)
: IRequestHandler<ReplacePlayoutTemplateItems, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(
ReplacePlayoutTemplateItems request,
CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.ProgramSchedule)
.Include(p => p.Templates)
.ThenInclude(t => t.Template)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout)
{
PlayoutTemplate[] existing = playout.Templates.ToArray();
List<ReplacePlayoutTemplate> incoming = request.Items;
var toAdd = incoming.Filter(x => existing.All(e => e.Id != x.Id)).ToList();
var toRemove = existing.Filter(e => incoming.All(m => m.Id != e.Id)).ToList();
var toUpdate = incoming.Except(toAdd).ToList();
foreach (PlayoutTemplate remove in toRemove)
{
playout.Templates.Remove(remove);
}
foreach (ReplacePlayoutTemplate add in toAdd)
{
playout.Templates.Add(
new PlayoutTemplate
{
PlayoutId = playout.Id,
Index = add.Index,
TemplateId = add.TemplateId,
DaysOfWeek = add.DaysOfWeek,
DaysOfMonth = add.DaysOfMonth,
MonthsOfYear = add.MonthsOfYear
});
}
foreach (ReplacePlayoutTemplate update in toUpdate)
{
foreach (PlayoutTemplate ex in existing.Filter(x => x.Id == update.Id))
{
ex.Index = update.Index;
ex.TemplateId = update.TemplateId;
ex.DaysOfWeek = update.DaysOfWeek;
ex.DaysOfMonth = update.DaysOfMonth;
ex.MonthsOfYear = update.MonthsOfYear;
}
}
await dbContext.SaveChangesAsync(cancellationToken);
}
return Option<BaseError>.None;
}
catch (Exception ex)
{
logger.LogError(ex, "Error saving playout template items");
return BaseError.New(ex.Message);
}
}
}

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

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

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

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.Scheduling;
public record ReplaceTemplateItems(int TemplateId, string Name, List<ReplaceTemplateItem> Items)
: IRequest<Either<BaseError, List<TemplateItemViewModel>>>;

64
ErsatzTV.Application/Scheduling/Commands/ReplaceTemplateItemsHandler.cs

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
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 ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<ReplaceTemplateItems, Either<BaseError, List<TemplateItemViewModel>>>
{
public async Task<Either<BaseError, List<TemplateItemViewModel>>> Handle(
ReplaceTemplateItems request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Validation<BaseError, Template> validation = await Validate(dbContext, request);
return await validation.Apply(ps => Persist(dbContext, request, ps));
}
private static async Task<List<TemplateItemViewModel>> Persist(
TvContext dbContext,
ReplaceTemplateItems request,
Template template)
{
template.Name = request.Name;
dbContext.RemoveRange(template.Items);
template.Items = request.Items.Map(i => BuildItem(template, 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(template)
.Collection(t => t.Items)
.Query()
.Include(i => i.Block)
.LoadAsync();
return template.Items.Map(Mapper.ProjectToViewModel).ToList();
}
private static TemplateItem BuildItem(Template template, ReplaceTemplateItem item) =>
new()
{
TemplateId = template.Id,
BlockId = item.BlockId,
StartTime = item.StartTime
};
private static Task<Validation<BaseError, Template>> Validate(TvContext dbContext, ReplaceTemplateItems request) =>
TemplateMustExist(dbContext, request.TemplateId);
private static Task<Validation<BaseError, Template>> TemplateMustExist(TvContext dbContext, int templateId) =>
dbContext.Templates
.Include(b => b.Items)
.SelectOneAsync(b => b.Id, b => b.Id == templateId)
.Map(o => o.ToValidation<BaseError>("[TemplateId] does not exist."));
}

56
ErsatzTV.Application/Scheduling/Mapper.cs

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Application.Scheduling;
internal static class Mapper
{
internal static BlockGroupViewModel ProjectToViewModel(BlockGroup blockGroup) =>
new(blockGroup.Id, blockGroup.Name, blockGroup.Blocks.Count);
internal static BlockViewModel ProjectToViewModel(Block block) =>
new(block.Id, block.Name, block.Minutes);
internal static BlockItemViewModel ProjectToViewModel(BlockItem blockItem) =>
new(
blockItem.Id,
blockItem.Index,
blockItem.CollectionType,
blockItem.Collection is not null ? MediaCollections.Mapper.ProjectToViewModel(blockItem.Collection) : null,
blockItem.MultiCollection is not null
? MediaCollections.Mapper.ProjectToViewModel(blockItem.MultiCollection)
: null,
blockItem.SmartCollection is not null
? MediaCollections.Mapper.ProjectToViewModel(blockItem.SmartCollection)
: null,
blockItem.MediaItem switch
{
Show show => MediaItems.Mapper.ProjectToViewModel(show),
Season season => MediaItems.Mapper.ProjectToViewModel(season),
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist),
_ => null
},
blockItem.PlaybackOrder);
internal static TemplateGroupViewModel ProjectToViewModel(TemplateGroup templateGroup) =>
new(templateGroup.Id, templateGroup.Name, templateGroup.Templates.Count);
internal static TemplateViewModel ProjectToViewModel(Template template) =>
new(template.Id, template.TemplateGroupId, template.Name);
internal static TemplateItemViewModel ProjectToViewModel(TemplateItem templateItem)
{
DateTime startTime = DateTime.Today.Add(templateItem.StartTime);
DateTime endTime = startTime.AddMinutes(templateItem.Block.Minutes);
return new TemplateItemViewModel(templateItem.BlockId, templateItem.Block.Name, startTime, endTime);
}
internal static PlayoutTemplateViewModel ProjectToViewModel(PlayoutTemplate playoutTemplate) =>
new(
playoutTemplate.Id,
ProjectToViewModel(playoutTemplate.Template),
playoutTemplate.Index,
playoutTemplate.DaysOfWeek,
playoutTemplate.DaysOfMonth,
playoutTemplate.MonthsOfYear);
}

9
ErsatzTV.Application/Scheduling/PlayoutTemplateViewModel.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
namespace ErsatzTV.Application.Scheduling;
public record PlayoutTemplateViewModel(
int Id,
TemplateViewModel Template,
int Index,
ICollection<DayOfWeek> DaysOfWeek,
ICollection<int> DaysOfMonth,
ICollection<int> MonthsOfYear);

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

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

21
ErsatzTV.Application/Scheduling/Queries/GetAllBlockGroupsHandler.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 GetAllBlockGroupsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllBlockGroups, List<BlockGroupViewModel>>
{
public async Task<List<BlockGroupViewModel>> Handle(GetAllBlockGroups request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<BlockGroup> blockGroups = await dbContext.BlockGroups
.AsNoTracking()
.Include(g => g.Blocks)
.ToListAsync(cancellationToken);
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();
}
}

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

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

23
ErsatzTV.Application/Scheduling/Queries/GetAllTemplateGroupsHandler.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 GetAllTemplateGroupsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllTemplateGroups, List<TemplateGroupViewModel>>
{
public async Task<List<TemplateGroupViewModel>> Handle(
GetAllTemplateGroups request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<TemplateGroup> blockGroups = await dbContext.TemplateGroups
.AsNoTracking()
.Include(g => g.Templates)
.ToListAsync(cancellationToken);
return blockGroups.Map(Mapper.ProjectToViewModel).ToList();
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetBlockById(int BlockId) : IRequest<Option<BlockViewModel>>;

17
ErsatzTV.Application/Scheduling/Queries/GetBlockByIdHandler.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 GetBlockByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetBlockById, Option<BlockViewModel>>
{
public async Task<Option<BlockViewModel>> Handle(GetBlockById request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Blocks
.SelectOneAsync(b => b.Id, b => b.Id == request.BlockId)
.MapT(Mapper.ProjectToViewModel);
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetBlockItems(int BlockId) : IRequest<List<BlockItemViewModel>>;

36
ErsatzTV.Application/Scheduling/Queries/GetBlockItemsHandler.cs

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetBlockItemsHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetBlockItems, List<BlockItemViewModel>>
{
public async Task<List<BlockItemViewModel>> Handle(GetBlockItems request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.BlockItems
.AsNoTracking()
.Filter(i => i.BlockId == request.BlockId)
.Include(i => i.Collection)
.Include(i => i.MultiCollection)
.Include(i => i.SmartCollection)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).SeasonMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Season).Show)
.ThenInclude(s => s.ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Show).ShowMetadata)
.ThenInclude(sm => sm.Artwork)
.Include(i => i.MediaItem)
.ThenInclude(i => (i as Artist).ArtistMetadata)
.ThenInclude(am => am.Artwork)
.ToListAsync(cancellationToken)
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetBlocksByBlockGroupId(int BlockGroupId) : IRequest<List<BlockViewModel>>;

21
ErsatzTV.Application/Scheduling/Queries/GetBlocksByBlockGroupIdHandler.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 GetBlocksByBlockGroupIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetBlocksByBlockGroupId, List<BlockViewModel>>
{
public async Task<List<BlockViewModel>> Handle(GetBlocksByBlockGroupId request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<Block> blocks = await dbContext.Blocks
.Filter(b => b.BlockGroupId == request.BlockGroupId)
.AsNoTracking()
.ToListAsync(cancellationToken);
return blocks.Map(Mapper.ProjectToViewModel).ToList();
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetPlayoutTemplates(int PlayoutId) : IRequest<List<PlayoutTemplateViewModel>>;

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

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetPlayoutTemplatesHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetPlayoutTemplates, List<PlayoutTemplateViewModel>>
{
public async Task<List<PlayoutTemplateViewModel>> Handle(
GetPlayoutTemplates request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<PlayoutTemplate> playoutTemplates = await dbContext.PlayoutTemplates
.AsNoTracking()
.Filter(t => t.PlayoutId == request.PlayoutId)
.Include(t => t.Template)
.ToListAsync(cancellationToken);
return playoutTemplates.Map(Mapper.ProjectToViewModel).ToList();
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetTemplateById(int TemplateId) : IRequest<Option<TemplateViewModel>>;

17
ErsatzTV.Application/Scheduling/Queries/GetTemplateByIdHandler.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 GetTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetTemplateById, Option<TemplateViewModel>>
{
public async Task<Option<TemplateViewModel>> Handle(GetTemplateById request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.Templates
.SelectOneAsync(b => b.Id, b => b.Id == request.TemplateId)
.MapT(Mapper.ProjectToViewModel);
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetTemplateItems(int TemplateId) : IRequest<List<TemplateItemViewModel>>;

19
ErsatzTV.Application/Scheduling/Queries/GetTemplateItemsHandler.cs

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Scheduling;
public class GetTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory) : IRequestHandler<GetTemplateItems, List<TemplateItemViewModel>>
{
public async Task<List<TemplateItemViewModel>> Handle(GetTemplateItems request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.TemplateItems
.AsNoTracking()
.Filter(i => i.TemplateId == request.TemplateId)
.Include(i => i.Block)
.ToListAsync(cancellationToken)
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList());
}
}

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

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record GetTemplatesByTemplateGroupId(int TemplateGroupId) : IRequest<List<TemplateViewModel>>;

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

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

3
ErsatzTV.Application/Scheduling/TemplateGroupViewModel.cs

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

3
ErsatzTV.Application/Scheduling/TemplateItemViewModel.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.Scheduling;
public record TemplateItemViewModel(int BlockId, string BlockName, DateTime StartTime, DateTime EndTime);

3
ErsatzTV.Application/Scheduling/TemplateViewModel.cs

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

6
ErsatzTV.Core/Domain/Playout.cs

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
namespace ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Domain;
public class Playout
{
@ -14,5 +16,7 @@ public class Playout @@ -14,5 +16,7 @@ public class Playout
public PlayoutAnchor Anchor { get; set; }
public List<PlayoutProgramScheduleAnchor> ProgramScheduleAnchors { get; set; }
public List<PlayoutScheduleItemFillGroupIndex> FillGroupIndices { get; set; }
public ICollection<PlayoutTemplate> Templates { get; set; }
public ICollection<PlayoutHistory> PlayoutHistory { get; set; }
public TimeSpan? DailyRebuildTime { get; set; }
}

1
ErsatzTV.Core/Domain/ProgramSchedulePlayoutType.cs

@ -4,6 +4,7 @@ public enum ProgramSchedulePlayoutType @@ -4,6 +4,7 @@ public enum ProgramSchedulePlayoutType
{
None = 0,
Flood = 1,
Block = 2,
ExternalJson = 20
}

13
ErsatzTV.Core/Domain/Scheduling/Block.cs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class Block
{
public int Id { get; set; }
public int BlockGroupId { get; set; }
public BlockGroup BlockGroup { get; set; }
public string Name { get; set; }
public int Minutes { get; set; }
public ICollection<BlockItem> Items { get; set; }
public ICollection<TemplateItem> TemplateItems { get; set; }
public ICollection<PlayoutHistory> PlayoutHistory { get; set; }
}

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

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

19
ErsatzTV.Core/Domain/Scheduling/BlockItem.cs

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class BlockItem
{
public int Id { get; set; }
public int Index { get; set; }
public int BlockId { get; set; }
public Block Block { get; set; }
public ProgramScheduleItemCollectionType CollectionType { get; set; }
public int? CollectionId { get; set; }
public Collection Collection { get; set; }
public int? MediaItemId { get; set; }
public MediaItem MediaItem { get; set; }
public int? MultiCollectionId { get; set; }
public MultiCollection MultiCollection { get; set; }
public int? SmartCollectionId { get; set; }
public SmartCollection SmartCollection { get; set; }
public PlaybackOrder PlaybackOrder { get; set; }
}

21
ErsatzTV.Core/Domain/Scheduling/PlayoutHistory.cs

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class PlayoutHistory
{
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int BlockId { get; set; }
public Block Block { get; set; }
// something that uniquely identifies the collection within the block
public string Key { get; set; }
// last occurence of an item from this collection in the playout
public DateTime When { get; set; }
// details about the item
public string Details { get; set; }
}

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

@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class PlayoutTemplate
{
public int Id { get; set; }
public int PlayoutId { get; set; }
public Playout Playout { get; set; }
public int TemplateId { get; set; }
public Template Template { get; set; }
public int Index { get; set; }
public ICollection<DayOfWeek> DaysOfWeek { get; set; }
public ICollection<int> DaysOfMonth { get; set; }
public ICollection<int> MonthsOfYear { get; set; }
public DateTimeOffset StartDate { get; set; }
public DateTimeOffset EndDate { get; set; }
// TODO: ICollection<DateTimeOffset> AdditionalDays { get; set; }
public static List<DayOfWeek> AllDaysOfWeek() =>
[
DayOfWeek.Monday,
DayOfWeek.Tuesday,
DayOfWeek.Wednesday,
DayOfWeek.Thursday,
DayOfWeek.Friday,
DayOfWeek.Saturday,
DayOfWeek.Sunday
];
public static List<int> AllDaysOfMonth() => Enumerable.Range(1, 31).ToList();
public static List<int> AllMonthsOfYear() => Enumerable.Range(1, 12).ToList();
}

11
ErsatzTV.Core/Domain/Scheduling/Template.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class Template
{
public int Id { get; set; }
public int TemplateGroupId { get; set; }
public TemplateGroup TemplateGroup { get; set; }
public string Name { get; set; }
public ICollection<TemplateItem> Items { get; set; }
public ICollection<PlayoutTemplate> PlayoutTemplates { get; set; }
}

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

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

11
ErsatzTV.Core/Domain/Scheduling/TemplateItem.cs

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
namespace ErsatzTV.Core.Domain.Scheduling;
public class TemplateItem
{
public int Id { get; set; }
public int TemplateId { get; set; }
public Template Template { get; set; }
public int BlockId { get; set; }
public Block Block { get; set; }
public TimeSpan StartTime { get; set; }
}

9
ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IBlockPlayoutBuilder
{
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
}

9
ErsatzTV.Core/Interfaces/Scheduling/IExternalJsonPlayoutBuilder.cs

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IExternalJsonPlayoutBuilder
{
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken);
}

367
ErsatzTV.Core/Scheduling/BlockPlayoutBuilder.cs

@ -0,0 +1,367 @@ @@ -0,0 +1,367 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Extensions;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace ErsatzTV.Core.Scheduling;
public class BlockPlayoutBuilder(
IConfigElementRepository configElementRepository,
IMediaCollectionRepository mediaCollectionRepository,
ITelevisionRepository televisionRepository,
IArtistRepository artistRepository,
ILogger<BlockPlayoutBuilder> logger)
: IBlockPlayoutBuilder
{
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
{
logger.LogDebug(
"Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}",
playout.Id,
playout.Channel.Number,
playout.Channel.Name);
DateTimeOffset start = DateTimeOffset.Now;
// get blocks to schedule
List<RealBlock> blocksToSchedule = await GetBlocksToSchedule(playout, start);
// get all collection items for the playout
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule);
// TODO: REMOVE THIS !!!
playout.Items.Clear();
// TODO: REMOVE THIS !!!
var historyToRemove = playout.PlayoutHistory
.Filter(h => h.When > start.UtcDateTime)
.ToList();
foreach (PlayoutHistory remove in historyToRemove)
{
playout.PlayoutHistory.Remove(remove);
}
foreach (RealBlock realBlock in blocksToSchedule)
{
logger.LogDebug(
"Will schedule block {Block} at {Start}",
realBlock.Block.Name,
realBlock.Start);
DateTimeOffset currentTime = realBlock.Start;
foreach (BlockItem blockItem in realBlock.Block.Items)
{
// TODO: support other playback orders
if (blockItem.PlaybackOrder is not PlaybackOrder.SeasonEpisode and not PlaybackOrder.Chronological)
{
continue;
}
// TODO: check if change is needed - if not, skip building
// - block can change
// - template can change
// - playout templates can change
// TODO: check for playout history for this collection
string historyKey = HistoryKey.ForBlockItem(blockItem);
logger.LogDebug("History key for block item {Item} is {Key}", blockItem.Id, historyKey);
DateTime historyTime = currentTime.UtcDateTime;
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory
.Filter(h => h.BlockId == blockItem.BlockId)
.Filter(h => h.Key == historyKey)
.Filter(h => h.When < historyTime)
.OrderByDescending(h => h.When)
.HeadOrNone();
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 };
var collectionKey = CollectionKey.ForBlockItem(blockItem);
List<MediaItem> collectionItems = collectionMediaItems[collectionKey];
// get enumerator
var enumerator = new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state);
// seek to the appropriate place in the collection enumerator
foreach (PlayoutHistory history in maybeHistory)
{
logger.LogDebug("History is applicable: {When}: {History}", history.When, history.Details);
// find next media item
HistoryDetails.Details details = JsonConvert.DeserializeObject<HistoryDetails.Details>(history.Details);
if (details.SeasonNumber.HasValue && details.EpisodeNumber.HasValue)
{
Option<MediaItem> maybeMatchedItem = Optional(
collectionItems.Find(
ci => ci is Episode e &&
e.EpisodeMetadata.Any(em => em.EpisodeNumber == details.EpisodeNumber.Value) &&
e.Season.SeasonNumber == details.SeasonNumber.Value));
var copy = collectionItems.ToList();
if (maybeMatchedItem.IsNone)
{
var fakeItem = new Episode
{
Season = new Season { SeasonNumber = details.SeasonNumber.Value },
EpisodeMetadata =
[
new EpisodeMetadata
{
EpisodeNumber = details.EpisodeNumber.Value,
ReleaseDate = details.ReleaseDate
}
]
};
copy.Add(fakeItem);
maybeMatchedItem = fakeItem;
}
foreach (MediaItem matchedItem in maybeMatchedItem)
{
IComparer<MediaItem> comparer = blockItem.PlaybackOrder switch
{
PlaybackOrder.Chronological => new ChronologicalMediaComparer(),
_ => new SeasonEpisodeMediaComparer()
};
copy.Sort(comparer);
state.Index = copy.IndexOf(matchedItem);
enumerator.ResetState(state);
enumerator.MoveNext();
}
}
}
foreach (MediaItem mediaItem in enumerator.Current)
{
logger.LogDebug("current item: {Id} / {Title}", mediaItem.Id, mediaItem is Episode e ? GetTitle(e) : string.Empty);
TimeSpan itemDuration = DurationForMediaItem(mediaItem);
// TODO: create a playout item
var playoutItem = new PlayoutItem
{
MediaItemId = mediaItem.Id,
Start = currentTime.UtcDateTime,
Finish = currentTime.UtcDateTime + itemDuration,
InPoint = TimeSpan.Zero,
OutPoint = itemDuration,
FillerKind = FillerKind.None,
//CustomTitle = scheduleItem.CustomTitle,
//WatermarkId = scheduleItem.WatermarkId,
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
//SubtitleMode = scheduleItem.SubtitleMode
};
playout.Items.Add(playoutItem);
// TODO: create a playout history record
var nextHistory = new PlayoutHistory
{
PlayoutId = playout.Id,
BlockId = blockItem.BlockId,
When = currentTime.UtcDateTime,
Key = historyKey,
Details = HistoryDetails.ForMediaItem(mediaItem)
};
playout.PlayoutHistory.Add(nextHistory);
currentTime += itemDuration;
enumerator.MoveNext();
}
}
}
CleanUpHistory(playout, start);
return playout;
}
private static string GetTitle(Episode e)
{
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone()
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty);
var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList();
var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList();
if (episodeNumbers.Count == 0 || episodeTitles.Count == 0)
{
return "[unknown episode]";
}
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}";
var titlesString = $"{string.Join('/', episodeTitles)}";
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}";
}
private async Task<List<RealBlock>> GetBlocksToSchedule(Playout playout, DateTimeOffset start)
{
int daysToBuild = await configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
.IfNoneAsync(2);
DateTimeOffset finish = start.AddDays(daysToBuild);
var realBlocks = new List<RealBlock>();
DateTimeOffset current = start.Date;
while (current < finish)
{
foreach (PlayoutTemplate playoutTemplate in PlayoutTemplateSelector.GetPlayoutTemplateFor(playout.Templates, current))
{
// logger.LogDebug(
// "Will schedule day {Date} using template {Template}",
// current,
// playoutTemplate.Template.Name);
foreach (TemplateItem templateItem in playoutTemplate.Template.Items)
{
var realBlock = new RealBlock(
templateItem.Block,
new DateTimeOffset(
current.Year,
current.Month,
current.Day,
templateItem.StartTime.Hours,
templateItem.StartTime.Minutes,
0,
start.Offset));
realBlocks.Add(realBlock);
}
current = current.AddDays(1);
}
}
realBlocks.RemoveAll(b => b.Start.AddMinutes(b.Block.Minutes) < start || b.Start > finish);
return realBlocks;
}
private void CleanUpHistory(Playout playout, DateTimeOffset start)
{
var groups = new Dictionary<string, List<PlayoutHistory>>();
foreach (PlayoutHistory history in playout.PlayoutHistory)
{
var key = $"{history.BlockId}-{history.Key}";
if (!groups.TryGetValue(key, out List<PlayoutHistory> group))
{
group = [];
groups[key] = group;
}
group.Add(history);
}
foreach ((string key, List<PlayoutHistory> group) in groups)
{
logger.LogDebug("History key {Key} has {Count} items in group", key, group.Count);
IEnumerable<PlayoutHistory> toDelete = group
.Filter(h => h.When < start.UtcDateTime)
.OrderByDescending(h => h.When)
.Tail();
foreach (PlayoutHistory delete in toDelete)
{
playout.PlayoutHistory.Remove(delete);
}
}
}
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(List<RealBlock> realBlocks)
{
var collectionKeys = realBlocks.Map(b => b.Block.Items)
.Flatten()
.DistinctBy(i => i.Id)
.Map(CollectionKey.ForBlockItem)
.Distinct()
.ToList();
IEnumerable<Tuple<CollectionKey, List<MediaItem>>> tuples = await collectionKeys.Map(
async collectionKey => Tuple(
collectionKey,
await MediaItemsForCollection.Collect(
mediaCollectionRepository,
televisionRepository,
artistRepository,
collectionKey))).SequenceParallel();
return LanguageExt.Map.createRange(tuples);
}
private static TimeSpan DurationForMediaItem(MediaItem mediaItem)
{
MediaVersion version = mediaItem.GetHeadVersion();
return version.Duration;
}
private record RealBlock(Block Block, DateTimeOffset Start);
private static class HistoryKey
{
private static readonly JsonSerializerSettings Settings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public static string ForBlockItem(BlockItem blockItem)
{
dynamic key = new
{
blockItem.BlockId,
blockItem.PlaybackOrder,
blockItem.CollectionType,
blockItem.CollectionId,
blockItem.MultiCollectionId,
blockItem.SmartCollectionId,
blockItem.MediaItemId
};
return JsonConvert.SerializeObject(key, Formatting.None, Settings);
}
}
private static class HistoryDetails
{
private static readonly JsonSerializerSettings Settings = new()
{
NullValueHandling = NullValueHandling.Ignore
};
public static string ForMediaItem(MediaItem mediaItem)
{
Details details = mediaItem switch
{
Episode e => ForEpisode(e),
_ => new Details(mediaItem.Id, null, null, null)
};
return JsonConvert.SerializeObject(details, Formatting.None, Settings);
}
private static Details ForEpisode(Episode e)
{
int? episodeNumber = null;
DateTime? releaseDate = null;
foreach (EpisodeMetadata episodeMetadata in e.EpisodeMetadata.HeadOrNone())
{
episodeNumber = episodeMetadata.EpisodeNumber;
releaseDate = episodeMetadata.ReleaseDate;
}
return new Details(e.Id, releaseDate, e.Season.SeasonNumber, episodeNumber);
}
public record Details(int? MediaItemId, DateTime? ReleaseDate, int? SeasonNumber, int? EpisodeNumber);
}
}

41
ErsatzTV.Core/Scheduling/CollectionKey.cs

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Scheduling;
@ -12,6 +13,46 @@ public class CollectionKey : Record<CollectionKey> @@ -12,6 +13,46 @@ public class CollectionKey : Record<CollectionKey>
public int? MediaItemId { get; set; }
public string FakeCollectionKey { get; set; }
public static CollectionKey ForBlockItem(BlockItem item) =>
item.CollectionType switch
{
ProgramScheduleItemCollectionType.Collection => new CollectionKey
{
CollectionType = item.CollectionType,
CollectionId = item.CollectionId,
},
ProgramScheduleItemCollectionType.TelevisionShow => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId,
},
ProgramScheduleItemCollectionType.TelevisionSeason => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId,
},
ProgramScheduleItemCollectionType.Artist => new CollectionKey
{
CollectionType = item.CollectionType,
MediaItemId = item.MediaItemId,
},
ProgramScheduleItemCollectionType.MultiCollection => new CollectionKey
{
CollectionType = item.CollectionType,
MultiCollectionId = item.MultiCollectionId,
},
ProgramScheduleItemCollectionType.SmartCollection => new CollectionKey
{
CollectionType = item.CollectionType,
SmartCollectionId = item.SmartCollectionId,
},
ProgramScheduleItemCollectionType.FakeCollection => new CollectionKey
{
CollectionType = item.CollectionType,
},
_ => throw new ArgumentOutOfRangeException(nameof(item))
};
public static CollectionKey ForScheduleItem(ProgramScheduleItem item) =>
item.CollectionType switch
{

19
ErsatzTV.Core/Scheduling/ExternalJsonPlayoutBuilder.cs

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Scheduling;
using Microsoft.Extensions.Logging;
namespace ErsatzTV.Core.Scheduling;
public class ExternalJsonPlayoutBuilder(ILogger<ExternalJsonPlayoutBuilder> logger) : IExternalJsonPlayoutBuilder
{
// nothing to do for external json playouts
public Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
{
logger.LogDebug(
"Building external json playout for channel {Number} - {Name}",
playout.Channel.Number,
playout.Channel.Name);
return Task.FromResult(playout);
}
}

7
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -43,10 +43,11 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -43,10 +43,11 @@ public class PlayoutBuilder : IPlayoutBuilder
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken)
{
if (playout.ProgramSchedulePlayoutType is ProgramSchedulePlayoutType.ExternalJson)
if (playout.ProgramSchedulePlayoutType is not ProgramSchedulePlayoutType.Flood)
{
_logger.LogDebug(
"Skipping external json playout build on channel {Number} - {Name}",
_logger.LogWarning(
"Skipping playout build with type {Type} on channel {Number} - {Name}",
playout.ProgramSchedulePlayoutType,
playout.Channel.Number,
playout.Channel.Name);

34
ErsatzTV.Core/Scheduling/PlayoutTemplateSelector.cs

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
using ErsatzTV.Core.Domain.Scheduling;
namespace ErsatzTV.Core.Scheduling;
public static class PlayoutTemplateSelector
{
public static Option<PlayoutTemplate> GetPlayoutTemplateFor(
IEnumerable<PlayoutTemplate> templates,
DateTimeOffset date)
{
foreach (PlayoutTemplate template in templates.OrderBy(x => x.Index))
{
bool daysOfWeek = template.DaysOfWeek.Contains(date.DayOfWeek);
if (!daysOfWeek)
{
continue;
}
bool daysOfMonth = template.DaysOfMonth.Contains(date.Day);
if (!daysOfMonth)
{
continue;
}
bool monthOfYear = template.MonthsOfYear.Contains(date.Month);
if (monthOfYear)
{
return template;
}
}
return Option<PlayoutTemplate>.None;
}
}

2
ErsatzTV.Infrastructure.MySql/ErsatzTV.Infrastructure.MySql.csproj

@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
</ItemGroup>

4895
ErsatzTV.Infrastructure.MySql/Migrations/20240114034944_Add_BlockScheduling.Designer.cs generated

File diff suppressed because it is too large Load Diff

355
ErsatzTV.Infrastructure.MySql/Migrations/20240114034944_Add_BlockScheduling.cs

@ -0,0 +1,355 @@ @@ -0,0 +1,355 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_BlockScheduling : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BlockGroup",
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_BlockGroup", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "TemplateGroup",
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_TemplateGroup", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Block",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
BlockGroupId = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Minutes = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Block", x => x.Id);
table.ForeignKey(
name: "FK_Block_BlockGroup_BlockGroupId",
column: x => x.BlockGroupId,
principalTable: "BlockGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Template",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
TemplateGroupId = table.Column<int>(type: "int", nullable: false),
Name = table.Column<string>(type: "varchar(255)", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Template", x => x.Id);
table.ForeignKey(
name: "FK_Template_TemplateGroup_TemplateGroupId",
column: x => x.TemplateGroupId,
principalTable: "TemplateGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "BlockItem",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Index = table.Column<int>(type: "int", nullable: false),
BlockId = table.Column<int>(type: "int", nullable: false),
CollectionType = table.Column<int>(type: "int", nullable: false),
CollectionId = table.Column<int>(type: "int", nullable: true),
MediaItemId = table.Column<int>(type: "int", nullable: true),
MultiCollectionId = table.Column<int>(type: "int", nullable: true),
SmartCollectionId = table.Column<int>(type: "int", nullable: true),
PlaybackOrder = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BlockItem", x => x.Id);
table.ForeignKey(
name: "FK_BlockItem_Block_BlockId",
column: x => x.BlockId,
principalTable: "Block",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_Collection_CollectionId",
column: x => x.CollectionId,
principalTable: "Collection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_MediaItem_MediaItemId",
column: x => x.MediaItemId,
principalTable: "MediaItem",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_MultiCollection_MultiCollectionId",
column: x => x.MultiCollectionId,
principalTable: "MultiCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_SmartCollection_SmartCollectionId",
column: x => x.SmartCollectionId,
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PlayoutHistory",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
PlayoutId = table.Column<int>(type: "int", nullable: false),
BlockId = table.Column<int>(type: "int", nullable: false),
Key = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
When = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Details = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutHistory", x => x.Id);
table.ForeignKey(
name: "FK_PlayoutHistory_Block_BlockId",
column: x => x.BlockId,
principalTable: "Block",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PlayoutHistory_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PlayoutTemplate",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
PlayoutId = table.Column<int>(type: "int", nullable: false),
TemplateId = table.Column<int>(type: "int", nullable: false),
Index = table.Column<int>(type: "int", nullable: false),
DaysOfWeek = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
DaysOfMonth = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
MonthsOfYear = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
StartDate = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false),
EndDate = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutTemplate", x => x.Id);
table.ForeignKey(
name: "FK_PlayoutTemplate_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PlayoutTemplate_Template_TemplateId",
column: x => x.TemplateId,
principalTable: "Template",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "TemplateItem",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
TemplateId = table.Column<int>(type: "int", nullable: false),
BlockId = table.Column<int>(type: "int", nullable: false),
StartTime = table.Column<TimeSpan>(type: "time(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TemplateItem", x => x.Id);
table.ForeignKey(
name: "FK_TemplateItem_Block_BlockId",
column: x => x.BlockId,
principalTable: "Block",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_TemplateItem_Template_TemplateId",
column: x => x.TemplateId,
principalTable: "Template",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Block_BlockGroupId",
table: "Block",
column: "BlockGroupId");
migrationBuilder.CreateIndex(
name: "IX_Block_Name",
table: "Block",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BlockGroup_Name",
table: "BlockGroup",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BlockItem_BlockId",
table: "BlockItem",
column: "BlockId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_CollectionId",
table: "BlockItem",
column: "CollectionId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_MediaItemId",
table: "BlockItem",
column: "MediaItemId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_MultiCollectionId",
table: "BlockItem",
column: "MultiCollectionId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_SmartCollectionId",
table: "BlockItem",
column: "SmartCollectionId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutHistory_BlockId",
table: "PlayoutHistory",
column: "BlockId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutHistory_PlayoutId",
table: "PlayoutHistory",
column: "PlayoutId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutTemplate_PlayoutId",
table: "PlayoutTemplate",
column: "PlayoutId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutTemplate_TemplateId",
table: "PlayoutTemplate",
column: "TemplateId");
migrationBuilder.CreateIndex(
name: "IX_Template_Name",
table: "Template",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Template_TemplateGroupId",
table: "Template",
column: "TemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_TemplateGroup_Name",
table: "TemplateGroup",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TemplateItem_BlockId",
table: "TemplateItem",
column: "BlockId");
migrationBuilder.CreateIndex(
name: "IX_TemplateItem_TemplateId",
table: "TemplateItem",
column: "TemplateId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BlockItem");
migrationBuilder.DropTable(
name: "PlayoutHistory");
migrationBuilder.DropTable(
name: "PlayoutTemplate");
migrationBuilder.DropTable(
name: "TemplateItem");
migrationBuilder.DropTable(
name: "Block");
migrationBuilder.DropTable(
name: "Template");
migrationBuilder.DropTable(
name: "BlockGroup");
migrationBuilder.DropTable(
name: "TemplateGroup");
}
}
}

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

@ -16,7 +16,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -16,7 +16,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.0")
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
@ -1820,6 +1820,225 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -1820,6 +1820,225 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.ToTable("Resolution", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("BlockGroupId")
.HasColumnType("int");
b.Property<int>("Minutes")
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("BlockGroupId");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Block", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("BlockGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("BlockId")
.HasColumnType("int");
b.Property<int?>("CollectionId")
.HasColumnType("int");
b.Property<int>("CollectionType")
.HasColumnType("int");
b.Property<int>("Index")
.HasColumnType("int");
b.Property<int?>("MediaItemId")
.HasColumnType("int");
b.Property<int?>("MultiCollectionId")
.HasColumnType("int");
b.Property<int>("PlaybackOrder")
.HasColumnType("int");
b.Property<int?>("SmartCollectionId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BlockId");
b.HasIndex("CollectionId");
b.HasIndex("MediaItemId");
b.HasIndex("MultiCollectionId");
b.HasIndex("SmartCollectionId");
b.ToTable("BlockItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("BlockId")
.HasColumnType("int");
b.Property<string>("Details")
.HasColumnType("longtext");
b.Property<string>("Key")
.HasColumnType("longtext");
b.Property<int>("PlayoutId")
.HasColumnType("int");
b.Property<DateTime>("When")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.HasIndex("BlockId");
b.HasIndex("PlayoutId");
b.ToTable("PlayoutHistory", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("DaysOfMonth")
.HasColumnType("longtext");
b.Property<string>("DaysOfWeek")
.HasColumnType("longtext");
b.Property<DateTimeOffset>("EndDate")
.HasColumnType("datetime(6)");
b.Property<int>("Index")
.HasColumnType("int");
b.Property<string>("MonthsOfYear")
.HasColumnType("longtext");
b.Property<int>("PlayoutId")
.HasColumnType("int");
b.Property<DateTimeOffset>("StartDate")
.HasColumnType("datetime(6)");
b.Property<int>("TemplateId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("PlayoutId");
b.HasIndex("TemplateId");
b.ToTable("PlayoutTemplate", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.Property<int>("TemplateGroupId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("TemplateGroupId");
b.ToTable("Template", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<string>("Name")
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("TemplateGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<int>("BlockId")
.HasColumnType("int");
b.Property<TimeSpan>("StartTime")
.HasColumnType("time(6)");
b.Property<int>("TemplateId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("BlockId");
b.HasIndex("TemplateId");
b.ToTable("TemplateItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b =>
{
b.Property<int>("Id")
@ -3634,6 +3853,124 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -3634,6 +3853,124 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockGroup", "BlockGroup")
.WithMany("Blocks")
.HasForeignKey("BlockGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BlockGroup");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("Items")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection")
.WithMany()
.HasForeignKey("CollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
.WithMany()
.HasForeignKey("MediaItemId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection")
.WithMany()
.HasForeignKey("MultiCollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection")
.WithMany()
.HasForeignKey("SmartCollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Block");
b.Navigation("Collection");
b.Navigation("MediaItem");
b.Navigation("MultiCollection");
b.Navigation("SmartCollection");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("PlayoutHistory")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("PlayoutHistory")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Block");
b.Navigation("Playout");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Templates")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template")
.WithMany("PlayoutTemplates")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playout");
b.Navigation("Template");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", "TemplateGroup")
.WithMany("Templates")
.HasForeignKey("TemplateGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("TemplateGroup");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("TemplateItems")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template")
.WithMany("Items")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Block");
b.Navigation("Template");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Season", "Season")
@ -4353,9 +4690,13 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4353,9 +4690,13 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("Items");
b.Navigation("PlayoutHistory");
b.Navigation("ProgramScheduleAlternates");
b.Navigation("ProgramScheduleAnchors");
b.Navigation("Templates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
@ -4367,6 +4708,32 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -4367,6 +4708,32 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Navigation("ProgramScheduleAlternates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b =>
{
b.Navigation("Items");
b.Navigation("PlayoutHistory");
b.Navigation("TemplateItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b =>
{
b.Navigation("Blocks");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.Navigation("Items");
b.Navigation("PlayoutTemplates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b =>
{
b.Navigation("Templates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b =>
{
b.Navigation("Actors");

6
ErsatzTV.Infrastructure.Sqlite/ErsatzTV.Infrastructure.Sqlite.csproj

@ -12,9 +12,9 @@ @@ -12,9 +12,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.24" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Dapper" Version="2.1.28" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
</ItemGroup>

4673
ErsatzTV.Infrastructure.Sqlite/Migrations/20240110162335_Add_Block.Designer.cs generated

File diff suppressed because it is too large Load Diff

153
ErsatzTV.Infrastructure.Sqlite/Migrations/20240110162335_Add_Block.cs

@ -0,0 +1,153 @@ @@ -0,0 +1,153 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Block : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BlockGroup",
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_BlockGroup", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Block",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BlockGroupId = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
Minutes = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Block", x => x.Id);
table.ForeignKey(
name: "FK_Block_BlockGroup_BlockGroupId",
column: x => x.BlockGroupId,
principalTable: "BlockGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "BlockItem",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Index = table.Column<int>(type: "INTEGER", nullable: false),
BlockId = table.Column<int>(type: "INTEGER", nullable: false),
CollectionType = table.Column<int>(type: "INTEGER", nullable: false),
CollectionId = table.Column<int>(type: "INTEGER", nullable: true),
MediaItemId = table.Column<int>(type: "INTEGER", nullable: true),
MultiCollectionId = table.Column<int>(type: "INTEGER", nullable: true),
SmartCollectionId = table.Column<int>(type: "INTEGER", nullable: true),
PlaybackOrder = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BlockItem", x => x.Id);
table.ForeignKey(
name: "FK_BlockItem_Block_BlockId",
column: x => x.BlockId,
principalTable: "Block",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_Collection_CollectionId",
column: x => x.CollectionId,
principalTable: "Collection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_MediaItem_MediaItemId",
column: x => x.MediaItemId,
principalTable: "MediaItem",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_MultiCollection_MultiCollectionId",
column: x => x.MultiCollectionId,
principalTable: "MultiCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BlockItem_SmartCollection_SmartCollectionId",
column: x => x.SmartCollectionId,
principalTable: "SmartCollection",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Block_BlockGroupId",
table: "Block",
column: "BlockGroupId");
migrationBuilder.CreateIndex(
name: "IX_Block_Name",
table: "Block",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BlockGroup_Name",
table: "BlockGroup",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BlockItem_BlockId",
table: "BlockItem",
column: "BlockId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_CollectionId",
table: "BlockItem",
column: "CollectionId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_MediaItemId",
table: "BlockItem",
column: "MediaItemId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_MultiCollectionId",
table: "BlockItem",
column: "MultiCollectionId");
migrationBuilder.CreateIndex(
name: "IX_BlockItem_SmartCollectionId",
table: "BlockItem",
column: "SmartCollectionId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BlockItem");
migrationBuilder.DropTable(
name: "Block");
migrationBuilder.DropTable(
name: "BlockGroup");
}
}
}

4789
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113002246_Add_Template.Designer.cs generated

File diff suppressed because it is too large Load Diff

126
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113002246_Add_Template.cs

@ -0,0 +1,126 @@ @@ -0,0 +1,126 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_Template : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TemplateGroup",
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_TemplateGroup", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Template",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
TemplateGroupId = table.Column<int>(type: "INTEGER", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
PlayoutId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Template", x => x.Id);
table.ForeignKey(
name: "FK_Template_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id");
table.ForeignKey(
name: "FK_Template_TemplateGroup_TemplateGroupId",
column: x => x.TemplateGroupId,
principalTable: "TemplateGroup",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "TemplateItem",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
TemplateId = table.Column<int>(type: "INTEGER", nullable: false),
BlockId = table.Column<int>(type: "INTEGER", nullable: false),
StartTime = table.Column<TimeSpan>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TemplateItem", x => x.Id);
table.ForeignKey(
name: "FK_TemplateItem_Block_BlockId",
column: x => x.BlockId,
principalTable: "Block",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_TemplateItem_Template_TemplateId",
column: x => x.TemplateId,
principalTable: "Template",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Template_Name",
table: "Template",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Template_PlayoutId",
table: "Template",
column: "PlayoutId");
migrationBuilder.CreateIndex(
name: "IX_Template_TemplateGroupId",
table: "Template",
column: "TemplateGroupId");
migrationBuilder.CreateIndex(
name: "IX_TemplateGroup_Name",
table: "TemplateGroup",
column: "Name",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_TemplateItem_BlockId",
table: "TemplateItem",
column: "BlockId");
migrationBuilder.CreateIndex(
name: "IX_TemplateItem_TemplateId",
table: "TemplateItem",
column: "TemplateId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TemplateItem");
migrationBuilder.DropTable(
name: "Template");
migrationBuilder.DropTable(
name: "TemplateGroup");
}
}
}

4837
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113121929_Add_PlayoutTemplate.Designer.cs generated

File diff suppressed because it is too large Load Diff

93
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113121929_Add_PlayoutTemplate.cs

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutTemplate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Template_Playout_PlayoutId",
table: "Template");
migrationBuilder.DropIndex(
name: "IX_Template_PlayoutId",
table: "Template");
migrationBuilder.DropColumn(
name: "PlayoutId",
table: "Template");
migrationBuilder.CreateTable(
name: "PlayoutTemplate",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PlayoutId = table.Column<int>(type: "INTEGER", nullable: false),
TemplateId = table.Column<int>(type: "INTEGER", nullable: false),
Index = table.Column<int>(type: "INTEGER", nullable: false),
DaysOfWeek = table.Column<string>(type: "TEXT", nullable: true),
MonthsOfYear = table.Column<string>(type: "TEXT", nullable: true),
StartDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
EndDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutTemplate", x => x.Id);
table.ForeignKey(
name: "FK_PlayoutTemplate_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PlayoutTemplate_Template_TemplateId",
column: x => x.TemplateId,
principalTable: "Template",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PlayoutTemplate_PlayoutId",
table: "PlayoutTemplate",
column: "PlayoutId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutTemplate_TemplateId",
table: "PlayoutTemplate",
column: "TemplateId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PlayoutTemplate");
migrationBuilder.AddColumn<int>(
name: "PlayoutId",
table: "Template",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Template_PlayoutId",
table: "Template",
column: "PlayoutId");
migrationBuilder.AddForeignKey(
name: "FK_Template_Playout_PlayoutId",
table: "Template",
column: "PlayoutId",
principalTable: "Playout",
principalColumn: "Id");
}
}
}

4840
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113140741_Add_PlayoutTemplate_DaysOfMonth.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113140741_Add_PlayoutTemplate_DaysOfMonth.cs

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutTemplate_DaysOfMonth : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DaysOfMonth",
table: "PlayoutTemplate",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DaysOfMonth",
table: "PlayoutTemplate");
}
}
}

4893
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113190523_Add_PlayoutHistory.Designer.cs generated

File diff suppressed because it is too large Load Diff

61
ErsatzTV.Infrastructure.Sqlite/Migrations/20240113190523_Add_PlayoutHistory.cs

@ -0,0 +1,61 @@ @@ -0,0 +1,61 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_PlayoutHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PlayoutHistory",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
PlayoutId = table.Column<int>(type: "INTEGER", nullable: false),
BlockId = table.Column<int>(type: "INTEGER", nullable: false),
Key = table.Column<string>(type: "TEXT", nullable: true),
When = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
Details = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlayoutHistory", x => x.Id);
table.ForeignKey(
name: "FK_PlayoutHistory_Block_BlockId",
column: x => x.BlockId,
principalTable: "Block",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PlayoutHistory_Playout_PlayoutId",
column: x => x.PlayoutId,
principalTable: "Playout",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PlayoutHistory_BlockId",
table: "PlayoutHistory",
column: "BlockId");
migrationBuilder.CreateIndex(
name: "IX_PlayoutHistory_PlayoutId",
table: "PlayoutHistory",
column: "PlayoutId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PlayoutHistory");
}
}
}

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

@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -15,7 +15,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
modelBuilder.HasAnnotation("ProductVersion", "8.0.1");
modelBuilder.Entity("ErsatzTV.Core.Domain.Actor", b =>
{
@ -715,7 +715,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -715,7 +715,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("SongMetadataId");
b.ToTable("Genre");
b.ToTable("Genre", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.JellyfinCollection", b =>
@ -1168,7 +1168,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1168,7 +1168,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("ArtistMetadataId");
b.ToTable("Mood");
b.ToTable("Mood", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MovieMetadata", b =>
@ -1276,7 +1276,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1276,7 +1276,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("SmartCollectionId");
b.ToTable("MultiCollectionSmartItem");
b.ToTable("MultiCollectionSmartItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoArtist", b =>
@ -1295,7 +1295,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1295,7 +1295,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("MusicVideoMetadataId");
b.ToTable("MusicVideoArtist");
b.ToTable("MusicVideoArtist", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.MusicVideoMetadata", b =>
@ -1818,6 +1818,225 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -1818,6 +1818,225 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.ToTable("Resolution", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("BlockGroupId")
.HasColumnType("INTEGER");
b.Property<int>("Minutes")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("BlockGroupId");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Block", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("BlockGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("BlockId")
.HasColumnType("INTEGER");
b.Property<int?>("CollectionId")
.HasColumnType("INTEGER");
b.Property<int>("CollectionType")
.HasColumnType("INTEGER");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<int?>("MediaItemId")
.HasColumnType("INTEGER");
b.Property<int?>("MultiCollectionId")
.HasColumnType("INTEGER");
b.Property<int>("PlaybackOrder")
.HasColumnType("INTEGER");
b.Property<int?>("SmartCollectionId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("BlockId");
b.HasIndex("CollectionId");
b.HasIndex("MediaItemId");
b.HasIndex("MultiCollectionId");
b.HasIndex("SmartCollectionId");
b.ToTable("BlockItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("BlockId")
.HasColumnType("INTEGER");
b.Property<string>("Details")
.HasColumnType("TEXT");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("When")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("BlockId");
b.HasIndex("PlayoutId");
b.ToTable("PlayoutHistory", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("DaysOfMonth")
.HasColumnType("TEXT");
b.Property<string>("DaysOfWeek")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("EndDate")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<string>("MonthsOfYear")
.HasColumnType("TEXT");
b.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("StartDate")
.HasColumnType("TEXT");
b.Property<int>("TemplateId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PlayoutId");
b.HasIndex("TemplateId");
b.ToTable("PlayoutTemplate", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int>("TemplateGroupId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("TemplateGroupId");
b.ToTable("Template", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("TemplateGroup", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("BlockId")
.HasColumnType("INTEGER");
b.Property<TimeSpan>("StartTime")
.HasColumnType("TEXT");
b.Property<int>("TemplateId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("BlockId");
b.HasIndex("TemplateId");
b.ToTable("TemplateItem", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b =>
{
b.Property<int>("Id")
@ -2055,7 +2274,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2055,7 +2274,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("ArtistMetadataId");
b.ToTable("Style");
b.ToTable("Style", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Subtitle", b =>
@ -2203,7 +2422,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2203,7 +2422,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("SongMetadataId");
b.ToTable("Tag");
b.ToTable("Tag", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.TraktList", b =>
@ -2344,7 +2563,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2344,7 +2563,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("EmbyLibraryId");
b.ToTable("EmbyPathInfo");
b.ToTable("EmbyPathInfo", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Jellyfin.JellyfinPathInfo", b =>
@ -2366,7 +2585,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2366,7 +2585,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.HasIndex("JellyfinLibraryId");
b.ToTable("JellyfinPathInfo");
b.ToTable("JellyfinPathInfo", (string)null);
});
modelBuilder.Entity("ErsatzTV.Core.Domain.EmbyLibrary", b =>
@ -3326,7 +3545,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3326,7 +3545,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasForeignKey("ProgramScheduleId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
b.OwnsOne("ErsatzTV.Core.Domain.Playout.Anchor#ErsatzTV.Core.Domain.PlayoutAnchor", "Anchor", b1 =>
{
b1.Property<int>("PlayoutId")
.HasColumnType("INTEGER");
@ -3356,7 +3575,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3356,7 +3575,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b1.WithOwner()
.HasForeignKey("PlayoutId");
b1.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 =>
b1.OwnsOne("ErsatzTV.Core.Domain.Playout.Anchor#ErsatzTV.Core.Domain.PlayoutAnchor.ScheduleItemsEnumeratorState#ErsatzTV.Core.Domain.CollectionEnumeratorState", "ScheduleItemsEnumeratorState", b2 =>
{
b2.Property<int>("PlayoutAnchorPlayoutId")
.HasColumnType("INTEGER");
@ -3439,7 +3658,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3439,7 +3658,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.HasForeignKey("SmartCollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutProgramScheduleAnchor.EnumeratorState#ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
{
b1.Property<int>("PlayoutProgramScheduleAnchorId")
.HasColumnType("INTEGER");
@ -3485,7 +3704,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3485,7 +3704,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.OwnsOne("ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
b.OwnsOne("ErsatzTV.Core.Domain.PlayoutScheduleItemFillGroupIndex.EnumeratorState#ErsatzTV.Core.Domain.CollectionEnumeratorState", "EnumeratorState", b1 =>
{
b1.Property<int>("PlayoutScheduleItemFillGroupIndexId")
.HasColumnType("INTEGER");
@ -3632,6 +3851,124 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -3632,6 +3851,124 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("Watermark");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.BlockGroup", "BlockGroup")
.WithMany("Blocks")
.HasForeignKey("BlockGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("BlockGroup");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("Items")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Collection", "Collection")
.WithMany()
.HasForeignKey("CollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MediaItem", "MediaItem")
.WithMany()
.HasForeignKey("MediaItemId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.MultiCollection", "MultiCollection")
.WithMany()
.HasForeignKey("MultiCollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("ErsatzTV.Core.Domain.SmartCollection", "SmartCollection")
.WithMany()
.HasForeignKey("SmartCollectionId")
.OnDelete(DeleteBehavior.Cascade);
b.Navigation("Block");
b.Navigation("Collection");
b.Navigation("MediaItem");
b.Navigation("MultiCollection");
b.Navigation("SmartCollection");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutHistory", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("PlayoutHistory")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("PlayoutHistory")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Block");
b.Navigation("Playout");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.PlayoutTemplate", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Playout", "Playout")
.WithMany("Templates")
.HasForeignKey("PlayoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template")
.WithMany("PlayoutTemplates")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playout");
b.Navigation("Template");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", "TemplateGroup")
.WithMany("Templates")
.HasForeignKey("TemplateGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("TemplateGroup");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateItem", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Block", "Block")
.WithMany("TemplateItems")
.HasForeignKey("BlockId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("ErsatzTV.Core.Domain.Scheduling.Template", "Template")
.WithMany("Items")
.HasForeignKey("TemplateId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Block");
b.Navigation("Template");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b =>
{
b.HasOne("ErsatzTV.Core.Domain.Season", "Season")
@ -4351,9 +4688,13 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4351,9 +4688,13 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("Items");
b.Navigation("PlayoutHistory");
b.Navigation("ProgramScheduleAlternates");
b.Navigation("ProgramScheduleAnchors");
b.Navigation("Templates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.ProgramSchedule", b =>
@ -4365,6 +4706,32 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -4365,6 +4706,32 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Navigation("ProgramScheduleAlternates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Block", b =>
{
b.Navigation("Items");
b.Navigation("PlayoutHistory");
b.Navigation("TemplateItems");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.BlockGroup", b =>
{
b.Navigation("Blocks");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.Template", b =>
{
b.Navigation("Items");
b.Navigation("PlayoutTemplates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.Scheduling.TemplateGroup", b =>
{
b.Navigation("Templates");
});
modelBuilder.Entity("ErsatzTV.Core.Domain.SeasonMetadata", b =>
{
b.Navigation("Actors");

10
ErsatzTV.Infrastructure/Data/Configurations/PlayoutConfiguration.cs

@ -34,5 +34,15 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout> @@ -34,5 +34,15 @@ public class PlayoutConfiguration : IEntityTypeConfiguration<Playout>
.WithOne(i => i.Playout)
.HasForeignKey(i => i.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(p => p.Templates)
.WithOne(i => i.Playout)
.HasForeignKey(i => i.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(p => p.PlayoutHistory)
.WithOne(h => h.Playout)
.HasForeignKey(h => h.PlayoutId)
.OnDelete(DeleteBehavior.Cascade);
}
}

31
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockConfiguration.cs

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class BlockConfiguration : IEntityTypeConfiguration<Block>
{
public void Configure(EntityTypeBuilder<Block> builder)
{
builder.ToTable("Block");
builder.HasIndex(b => b.Name)
.IsUnique();
builder.HasMany(b => b.Items)
.WithOne(i => i.Block)
.HasForeignKey(i => i.BlockId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(b => b.TemplateItems)
.WithOne(i => i.Block)
.HasForeignKey(i => i.BlockId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany(b => b.PlayoutHistory)
.WithOne(h => h.Block)
.HasForeignKey(h => h.BlockId)
.OnDelete(DeleteBehavior.Cascade);
}
}

21
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockGroupConfiguration.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 BlockGroupConfiguration : IEntityTypeConfiguration<BlockGroup>
{
public void Configure(EntityTypeBuilder<BlockGroup> builder)
{
builder.ToTable("BlockGroup");
builder.HasIndex(b => b.Name)
.IsUnique();
builder.HasMany(b => b.Blocks)
.WithOne(i => i.BlockGroup)
.HasForeignKey(i => i.BlockGroupId)
.OnDelete(DeleteBehavior.Cascade);
}
}

37
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/BlockItemConfiguration.cs

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class BlockItemConfiguration : IEntityTypeConfiguration<BlockItem>
{
public void Configure(EntityTypeBuilder<BlockItem> builder)
{
builder.ToTable("BlockItem");
builder.HasOne(i => i.Collection)
.WithMany()
.HasForeignKey(i => i.CollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.MediaItem)
.WithMany()
.HasForeignKey(i => i.MediaItemId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.MultiCollection)
.WithMany()
.HasForeignKey(i => i.MultiCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
builder.HasOne(i => i.SmartCollection)
.WithMany()
.HasForeignKey(i => i.SmartCollectionId)
.OnDelete(DeleteBehavior.Cascade)
.IsRequired(false);
}
}

13
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/PlayoutHistoryConfiguration.cs

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

22
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/PlayoutTemplateConfiguration.cs

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
using ErsatzTV.Core.Domain.Scheduling;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling;
public class PlayoutTemplateConfiguration : IEntityTypeConfiguration<PlayoutTemplate>
{
public void Configure(EntityTypeBuilder<PlayoutTemplate> builder)
{
builder.ToTable("PlayoutTemplate");
builder.Property(t => t.DaysOfMonth)
.HasConversion<IntCollectionValueConverter, CollectionValueComparer<int>>();
builder.Property(t => t.MonthsOfYear)
.HasConversion<IntCollectionValueConverter, CollectionValueComparer<int>>();
builder.Property(t => t.DaysOfWeek)
.HasConversion<EnumCollectionJsonValueConverter<DayOfWeek>, CollectionValueComparer<DayOfWeek>>();
}
}

26
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateConfiguration.cs

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

21
ErsatzTV.Infrastructure/Data/Configurations/Scheduling/TemplateGroupConfiguration.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 TemplateGroupConfiguration : IEntityTypeConfiguration<TemplateGroup>
{
public void Configure(EntityTypeBuilder<TemplateGroup> builder)
{
builder.ToTable("TemplateGroup");
builder.HasIndex(b => b.Name)
.IsUnique();
builder.HasMany(b => b.Templates)
.WithOne(i => i.TemplateGroup)
.HasForeignKey(i => i.TemplateGroupId)
.OnDelete(DeleteBehavior.Cascade);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save