mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* 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 changelogpull/1551/head
126 changed files with 34235 additions and 78 deletions
@ -1,5 +1,6 @@ |
|||||||
<Project> |
<Project> |
||||||
<PropertyGroup> |
<PropertyGroup> |
||||||
<InformationalVersion>develop</InformationalVersion> |
<InformationalVersion>develop</InformationalVersion> |
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> |
||||||
</PropertyGroup> |
</PropertyGroup> |
||||||
</Project> |
</Project> |
@ -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"); |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record BlockGroupViewModel(int Id, string Name, int BlockCount); |
@ -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); |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record BlockViewModel(int Id, string Name, int Minutes); |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record CreateBlock(int BlockGroupId, string Name) : IRequest<Either<BaseError, BlockViewModel>>; |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record CreateBlockGroup(string Name) : IRequest<Either<BaseError, BlockGroupViewModel>>; |
@ -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)); |
||||||
|
} |
@ -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)); |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record CreateTemplate(int TemplateGroupId, string Name) : IRequest<Either<BaseError, TemplateViewModel>>; |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record CreateTemplateGroup(string Name) : IRequest<Either<BaseError, TemplateGroupViewModel>>; |
@ -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)); |
||||||
|
} |
@ -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)); |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record DeleteBlock(int BlockId) : IRequest<Option<BaseError>>; |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record DeleteBlockGroup(int BlockGroupId) : IRequest<Option<BaseError>>; |
@ -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.")); |
||||||
|
} |
||||||
|
} |
@ -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.")); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record DeleteTemplate(int TemplateId) : IRequest<Option<BaseError>>; |
@ -0,0 +1,5 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record DeleteTemplateGroup(int TemplateGroupId) : IRequest<Option<BaseError>>; |
@ -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.")); |
||||||
|
} |
||||||
|
} |
@ -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.")); |
||||||
|
} |
||||||
|
} |
@ -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); |
@ -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>>>; |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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); |
@ -0,0 +1,6 @@ |
|||||||
|
using ErsatzTV.Core; |
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record ReplacePlayoutTemplateItems(int PlayoutId, List<ReplacePlayoutTemplate> Items) |
||||||
|
: IRequest<Option<BaseError>>; |
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record ReplaceTemplateItem(int BlockId, TimeSpan StartTime); |
@ -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>>>; |
@ -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.")); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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); |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetAllBlockGroups : IRequest<List<BlockGroupViewModel>>; |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetAllTemplateGroups : IRequest<List<TemplateGroupViewModel>>; |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetBlockById(int BlockId) : IRequest<Option<BlockViewModel>>; |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetBlockItems(int BlockId) : IRequest<List<BlockItemViewModel>>; |
@ -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()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetBlocksByBlockGroupId(int BlockGroupId) : IRequest<List<BlockViewModel>>; |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetPlayoutTemplates(int PlayoutId) : IRequest<List<PlayoutTemplateViewModel>>; |
@ -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(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetTemplateById(int TemplateId) : IRequest<Option<TemplateViewModel>>; |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetTemplateItems(int TemplateId) : IRequest<List<TemplateItemViewModel>>; |
@ -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()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record GetTemplatesByTemplateGroupId(int TemplateGroupId) : IRequest<List<TemplateViewModel>>; |
@ -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()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record TemplateGroupViewModel(int Id, string Name, int TemplateCount); |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record TemplateItemViewModel(int BlockId, string BlockName, DateTime StartTime, DateTime EndTime); |
@ -0,0 +1,3 @@ |
|||||||
|
namespace ErsatzTV.Application.Scheduling; |
||||||
|
|
||||||
|
public record TemplateViewModel(int Id, int TemplateGroupId, string Name); |
@ -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; } |
||||||
|
} |
@ -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; } |
||||||
|
} |
@ -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; } |
||||||
|
} |
@ -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; } |
||||||
|
} |
@ -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(); |
||||||
|
} |
@ -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; } |
||||||
|
} |
@ -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; } |
||||||
|
} |
@ -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; } |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -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"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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"); |
||||||
|
} |
||||||
|
} |
@ -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>>(); |
||||||
|
} |
||||||
|
} |
@ -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); |
||||||
|
} |
||||||
|
} |
@ -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…
Reference in new issue