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 @@
@@ -1,5 +1,6 @@
|
||||
<Project> |
||||
<PropertyGroup> |
||||
<InformationalVersion>develop</InformationalVersion> |
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> |
||||
</PropertyGroup> |
||||
</Project> |
@ -0,0 +1,67 @@
@@ -0,0 +1,67 @@
|
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application.Channels; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Channel = ErsatzTV.Core.Domain.Channel; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public class CreateBlockPlayoutHandler( |
||||
ChannelWriter<IBackgroundServiceRequest> channel, |
||||
IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<CreateBlockPlayout, Either<BaseError, CreatePlayoutResponse>> |
||||
{ |
||||
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle( |
||||
CreateBlockPlayout request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Playout> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(playout => PersistPlayout(dbContext, playout)); |
||||
} |
||||
|
||||
private async Task<CreatePlayoutResponse> PersistPlayout(TvContext dbContext, Playout playout) |
||||
{ |
||||
await dbContext.Playouts.AddAsync(playout); |
||||
await dbContext.SaveChangesAsync(); |
||||
await channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Reset)); |
||||
await channel.WriteAsync(new RefreshChannelList()); |
||||
return new CreatePlayoutResponse(playout.Id); |
||||
} |
||||
|
||||
private static async Task<Validation<BaseError, Playout>> Validate( |
||||
TvContext dbContext, |
||||
CreateBlockPlayout request) => |
||||
(await ValidateChannel(dbContext, request), ValidatePlayoutType(request)) |
||||
.Apply( |
||||
(channel, playoutType) => new Playout |
||||
{ |
||||
ChannelId = channel.Id, |
||||
ProgramSchedulePlayoutType = playoutType |
||||
}); |
||||
|
||||
private static Task<Validation<BaseError, Channel>> ValidateChannel( |
||||
TvContext dbContext, |
||||
CreateBlockPlayout createBlockPlayout) => |
||||
dbContext.Channels |
||||
.Include(c => c.Playouts) |
||||
.SelectOneAsync(c => c.Id, c => c.Id == createBlockPlayout.ChannelId) |
||||
.Map(o => o.ToValidation<BaseError>("Channel does not exist")) |
||||
.BindT(ChannelMustNotHavePlayouts); |
||||
|
||||
private static Validation<BaseError, Channel> ChannelMustNotHavePlayouts(Channel channel) => |
||||
Optional(channel.Playouts.Count) |
||||
.Filter(count => count == 0) |
||||
.Map(_ => channel) |
||||
.ToValidation<BaseError>("Channel already has one playout"); |
||||
|
||||
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType( |
||||
CreateBlockPlayout createBlockPlayout) => |
||||
Optional(createBlockPlayout.ProgramSchedulePlayoutType) |
||||
.Filter(playoutType => playoutType == ProgramSchedulePlayoutType.Block) |
||||
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must be Block"); |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record BlockGroupViewModel(int Id, string Name, int BlockCount); |
@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
|
||||
using ErsatzTV.Application.MediaCollections; |
||||
using ErsatzTV.Application.MediaItems; |
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record BlockItemViewModel( |
||||
int Id, |
||||
int Index, |
||||
ProgramScheduleItemCollectionType CollectionType, |
||||
MediaCollectionViewModel Collection, |
||||
MultiCollectionViewModel MultiCollection, |
||||
SmartCollectionViewModel SmartCollection, |
||||
NamedMediaItemViewModel MediaItem, |
||||
PlaybackOrder PlaybackOrder); |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record BlockViewModel(int Id, string Name, int Minutes); |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record CreateBlock(int BlockGroupId, string Name) : IRequest<Either<BaseError, BlockViewModel>>; |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record CreateBlockGroup(string Name) : IRequest<Either<BaseError, BlockGroupViewModel>>; |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class CreateBlockGroupHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<CreateBlockGroup, Either<BaseError, BlockGroupViewModel>> |
||||
{ |
||||
public async Task<Either<BaseError, BlockGroupViewModel>> Handle(CreateBlockGroup request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, BlockGroup> validation = await Validate(request); |
||||
return await validation.Apply(profile => PersistBlockGroup(dbContext, profile)); |
||||
} |
||||
|
||||
private static async Task<BlockGroupViewModel> PersistBlockGroup(TvContext dbContext, BlockGroup blockGroup) |
||||
{ |
||||
await dbContext.BlockGroups.AddAsync(blockGroup); |
||||
await dbContext.SaveChangesAsync(); |
||||
return Mapper.ProjectToViewModel(blockGroup); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, BlockGroup>> Validate(CreateBlockGroup request) => |
||||
Task.FromResult(ValidateName(request).Map(name => new BlockGroup { Name = name, Blocks = [] })); |
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateBlockGroup createBlockGroup) => |
||||
createBlockGroup.NotEmpty(x => x.Name) |
||||
.Bind(_ => createBlockGroup.NotLongerThan(50)(x => x.Name)); |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class CreateBlockHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<CreateBlock, Either<BaseError, BlockViewModel>> |
||||
{ |
||||
public async Task<Either<BaseError, BlockViewModel>> Handle( |
||||
CreateBlock request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Block> validation = await Validate(request); |
||||
return await validation.Apply(profile => PersistBlock(dbContext, profile)); |
||||
} |
||||
|
||||
private static async Task<BlockViewModel> PersistBlock(TvContext dbContext, Block block) |
||||
{ |
||||
await dbContext.Blocks.AddAsync(block); |
||||
await dbContext.SaveChangesAsync(); |
||||
return Mapper.ProjectToViewModel(block); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, Block>> Validate(CreateBlock request) => |
||||
Task.FromResult( |
||||
ValidateName(request).Map( |
||||
name => new Block |
||||
{ |
||||
BlockGroupId = request.BlockGroupId, |
||||
Name = name, |
||||
Minutes = 30 |
||||
})); |
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateBlock createBlock) => |
||||
createBlock.NotEmpty(x => x.Name) |
||||
.Bind(_ => createBlock.NotLongerThan(50)(x => x.Name)); |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record CreateTemplate(int TemplateGroupId, string Name) : IRequest<Either<BaseError, TemplateViewModel>>; |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record CreateTemplateGroup(string Name) : IRequest<Either<BaseError, TemplateGroupViewModel>>; |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class CreateTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<CreateTemplateGroup, Either<BaseError, TemplateGroupViewModel>> |
||||
{ |
||||
public async Task<Either<BaseError, TemplateGroupViewModel>> Handle( |
||||
CreateTemplateGroup request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, TemplateGroup> validation = await Validate(request); |
||||
return await validation.Apply(profile => PersistTemplateGroup(dbContext, profile)); |
||||
} |
||||
|
||||
private static async Task<TemplateGroupViewModel> PersistTemplateGroup( |
||||
TvContext dbContext, |
||||
TemplateGroup templateGroup) |
||||
{ |
||||
await dbContext.TemplateGroups.AddAsync(templateGroup); |
||||
await dbContext.SaveChangesAsync(); |
||||
return Mapper.ProjectToViewModel(templateGroup); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, TemplateGroup>> Validate(CreateTemplateGroup request) => |
||||
Task.FromResult(ValidateName(request).Map(name => new TemplateGroup { Name = name, Templates = [] })); |
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateTemplateGroup createTemplateGroup) => |
||||
createTemplateGroup.NotEmpty(x => x.Name) |
||||
.Bind(_ => createTemplateGroup.NotLongerThan(50)(x => x.Name)); |
||||
} |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class CreateTemplateHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<CreateTemplate, Either<BaseError, TemplateViewModel>> |
||||
{ |
||||
public async Task<Either<BaseError, TemplateViewModel>> Handle( |
||||
CreateTemplate request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Template> validation = await Validate(request); |
||||
return await validation.Apply(profile => PersistTemplate(dbContext, profile)); |
||||
} |
||||
|
||||
private static async Task<TemplateViewModel> PersistTemplate(TvContext dbContext, Template template) |
||||
{ |
||||
await dbContext.Templates.AddAsync(template); |
||||
await dbContext.SaveChangesAsync(); |
||||
return Mapper.ProjectToViewModel(template); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, Template>> Validate(CreateTemplate request) => |
||||
Task.FromResult( |
||||
ValidateName(request).Map( |
||||
name => new Template |
||||
{ |
||||
TemplateGroupId = request.TemplateGroupId, |
||||
Name = name |
||||
})); |
||||
|
||||
private static Validation<BaseError, string> ValidateName(CreateTemplate createTemplate) => |
||||
createTemplate.NotEmpty(x => x.Name) |
||||
.Bind(_ => createTemplate.NotLongerThan(50)(x => x.Name)); |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record DeleteBlock(int BlockId) : IRequest<Option<BaseError>>; |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record DeleteBlockGroup(int BlockGroupId) : IRequest<Option<BaseError>>; |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class DeleteBlockGroupHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<DeleteBlockGroup, Option<BaseError>> |
||||
{ |
||||
public async Task<Option<BaseError>> Handle(DeleteBlockGroup request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
Option<BlockGroup> maybeBlockGroup = await dbContext.BlockGroups |
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.BlockGroupId); |
||||
|
||||
foreach (BlockGroup blockGroup in maybeBlockGroup) |
||||
{ |
||||
dbContext.BlockGroups.Remove(blockGroup); |
||||
await dbContext.SaveChangesAsync(cancellationToken); |
||||
} |
||||
|
||||
return maybeBlockGroup.Match( |
||||
_ => Option<BaseError>.None, |
||||
() => BaseError.New($"BlockGroup {request.BlockGroupId} does not exist.")); |
||||
} |
||||
} |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class DeleteBlockHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<DeleteBlock, Option<BaseError>> |
||||
{ |
||||
public async Task<Option<BaseError>> Handle(DeleteBlock request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
Option<Block> maybeBlock = await dbContext.Blocks |
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.BlockId); |
||||
|
||||
foreach (Block block in maybeBlock) |
||||
{ |
||||
dbContext.Blocks.Remove(block); |
||||
await dbContext.SaveChangesAsync(cancellationToken); |
||||
} |
||||
|
||||
return maybeBlock.Match( |
||||
_ => Option<BaseError>.None, |
||||
() => BaseError.New($"Block {request.BlockId} does not exist.")); |
||||
} |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record DeleteTemplate(int TemplateId) : IRequest<Option<BaseError>>; |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record DeleteTemplateGroup(int TemplateGroupId) : IRequest<Option<BaseError>>; |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class DeleteTemplateGroupHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<DeleteTemplateGroup, Option<BaseError>> |
||||
{ |
||||
public async Task<Option<BaseError>> Handle(DeleteTemplateGroup request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
Option<TemplateGroup> maybeTemplateGroup = await dbContext.TemplateGroups |
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.TemplateGroupId); |
||||
|
||||
foreach (TemplateGroup templateGroup in maybeTemplateGroup) |
||||
{ |
||||
dbContext.TemplateGroups.Remove(templateGroup); |
||||
await dbContext.SaveChangesAsync(cancellationToken); |
||||
} |
||||
|
||||
return maybeTemplateGroup.Match( |
||||
_ => Option<BaseError>.None, |
||||
() => BaseError.New($"TemplateGroup {request.TemplateGroupId} does not exist.")); |
||||
} |
||||
} |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class DeleteTemplateHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<DeleteTemplate, Option<BaseError>> |
||||
{ |
||||
public async Task<Option<BaseError>> Handle(DeleteTemplate request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
Option<Template> maybeTemplate = await dbContext.Templates |
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.TemplateId); |
||||
|
||||
foreach (Template template in maybeTemplate) |
||||
{ |
||||
dbContext.Templates.Remove(template); |
||||
await dbContext.SaveChangesAsync(cancellationToken); |
||||
} |
||||
|
||||
return maybeTemplate.Match( |
||||
_ => Option<BaseError>.None, |
||||
() => BaseError.New($"Template {request.TemplateId} does not exist.")); |
||||
} |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ReplaceBlockItem( |
||||
int Index, |
||||
ProgramScheduleItemCollectionType CollectionType, |
||||
int? CollectionId, |
||||
int? MultiCollectionId, |
||||
int? SmartCollectionId, |
||||
int? MediaItemId, |
||||
PlaybackOrder PlaybackOrder); |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ReplaceBlockItems(int BlockId, string Name, int Minutes, List<ReplaceBlockItem> Items) |
||||
: IRequest<Either<BaseError, List<BlockItemViewModel>>>; |
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class ReplaceBlockItemsHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<ReplaceBlockItems, Either<BaseError, List<BlockItemViewModel>>> |
||||
{ |
||||
public async Task<Either<BaseError, List<BlockItemViewModel>>> Handle( |
||||
ReplaceBlockItems request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Block> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(ps => Persist(dbContext, request, ps)); |
||||
} |
||||
|
||||
private static async Task<List<BlockItemViewModel>> Persist( |
||||
TvContext dbContext, |
||||
ReplaceBlockItems request, |
||||
Block block) |
||||
{ |
||||
block.Name = request.Name; |
||||
block.Minutes = request.Minutes; |
||||
|
||||
dbContext.RemoveRange(block.Items); |
||||
block.Items = request.Items.Map(i => BuildItem(block, i.Index, i)).ToList(); |
||||
|
||||
await dbContext.SaveChangesAsync(); |
||||
|
||||
// TODO: refresh any playouts that use this schedule
|
||||
// foreach (Playout playout in programSchedule.Playouts)
|
||||
// {
|
||||
// await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh));
|
||||
// }
|
||||
|
||||
return block.Items.Map(Mapper.ProjectToViewModel).ToList(); |
||||
} |
||||
|
||||
private static BlockItem BuildItem(Block block, int index, ReplaceBlockItem item) => |
||||
new() |
||||
{ |
||||
BlockId = block.Id, |
||||
Index = index, |
||||
CollectionType = item.CollectionType, |
||||
CollectionId = item.CollectionId, |
||||
MultiCollectionId = item.MultiCollectionId, |
||||
SmartCollectionId = item.SmartCollectionId, |
||||
MediaItemId = item.MediaItemId, |
||||
PlaybackOrder = item.PlaybackOrder |
||||
}; |
||||
|
||||
private static Task<Validation<BaseError, Block>> Validate(TvContext dbContext, ReplaceBlockItems request) => |
||||
BlockMustExist(dbContext, request.BlockId) |
||||
.BindT(programSchedule => CollectionTypesMustBeValid(request, programSchedule)); |
||||
|
||||
private static Task<Validation<BaseError, Block>> BlockMustExist(TvContext dbContext, int blockId) => |
||||
dbContext.Blocks |
||||
.Include(b => b.Items) |
||||
.SelectOneAsync(b => b.Id, b => b.Id == blockId) |
||||
.Map(o => o.ToValidation<BaseError>("[BlockId] does not exist.")); |
||||
|
||||
private static Validation<BaseError, Block> CollectionTypesMustBeValid(ReplaceBlockItems request, Block block) => |
||||
request.Items.Map(item => CollectionTypeMustBeValid(item, block)).Sequence().Map(_ => block); |
||||
|
||||
private static Validation<BaseError, Block> CollectionTypeMustBeValid(ReplaceBlockItem item, Block block) |
||||
{ |
||||
switch (item.CollectionType) |
||||
{ |
||||
case ProgramScheduleItemCollectionType.Collection: |
||||
if (item.CollectionId is null) |
||||
{ |
||||
return BaseError.New("[Collection] is required for collection type 'Collection'"); |
||||
} |
||||
|
||||
break; |
||||
case ProgramScheduleItemCollectionType.TelevisionShow: |
||||
if (item.MediaItemId is null) |
||||
{ |
||||
return BaseError.New("[MediaItem] is required for collection type 'TelevisionShow'"); |
||||
} |
||||
|
||||
break; |
||||
case ProgramScheduleItemCollectionType.TelevisionSeason: |
||||
if (item.MediaItemId is null) |
||||
{ |
||||
return BaseError.New("[MediaItem] is required for collection type 'TelevisionSeason'"); |
||||
} |
||||
|
||||
break; |
||||
case ProgramScheduleItemCollectionType.Artist: |
||||
if (item.MediaItemId is null) |
||||
{ |
||||
return BaseError.New("[MediaItem] is required for collection type 'Artist'"); |
||||
} |
||||
|
||||
break; |
||||
case ProgramScheduleItemCollectionType.MultiCollection: |
||||
if (item.MultiCollectionId is null) |
||||
{ |
||||
return BaseError.New("[MultiCollection] is required for collection type 'MultiCollection'"); |
||||
} |
||||
|
||||
break; |
||||
case ProgramScheduleItemCollectionType.SmartCollection: |
||||
if (item.SmartCollectionId is null) |
||||
{ |
||||
return BaseError.New("[SmartCollection] is required for collection type 'SmartCollection'"); |
||||
} |
||||
|
||||
break; |
||||
case ProgramScheduleItemCollectionType.FakeCollection: |
||||
default: |
||||
return BaseError.New("[CollectionType] is invalid"); |
||||
} |
||||
|
||||
return block; |
||||
} |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ReplacePlayoutTemplate( |
||||
int Id, |
||||
int Index, |
||||
int TemplateId, |
||||
List<DayOfWeek> DaysOfWeek, |
||||
List<int> DaysOfMonth, |
||||
List<int> MonthsOfYear); |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ReplacePlayoutTemplateItems(int PlayoutId, List<ReplacePlayoutTemplate> Items) |
||||
: IRequest<Option<BaseError>>; |
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class ReplacePlayoutTemplateItemsHandler( |
||||
IDbContextFactory<TvContext> dbContextFactory, |
||||
ILogger<ReplacePlayoutTemplateItemsHandler> logger) |
||||
: IRequestHandler<ReplacePlayoutTemplateItems, Option<BaseError>> |
||||
{ |
||||
public async Task<Option<BaseError>> Handle( |
||||
ReplacePlayoutTemplateItems request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
try |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
Option<Playout> maybePlayout = await dbContext.Playouts |
||||
.Include(p => p.ProgramSchedule) |
||||
.Include(p => p.Templates) |
||||
.ThenInclude(t => t.Template) |
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId); |
||||
|
||||
foreach (Playout playout in maybePlayout) |
||||
{ |
||||
PlayoutTemplate[] existing = playout.Templates.ToArray(); |
||||
|
||||
List<ReplacePlayoutTemplate> incoming = request.Items; |
||||
|
||||
var toAdd = incoming.Filter(x => existing.All(e => e.Id != x.Id)).ToList(); |
||||
var toRemove = existing.Filter(e => incoming.All(m => m.Id != e.Id)).ToList(); |
||||
var toUpdate = incoming.Except(toAdd).ToList(); |
||||
|
||||
foreach (PlayoutTemplate remove in toRemove) |
||||
{ |
||||
playout.Templates.Remove(remove); |
||||
} |
||||
|
||||
foreach (ReplacePlayoutTemplate add in toAdd) |
||||
{ |
||||
playout.Templates.Add( |
||||
new PlayoutTemplate |
||||
{ |
||||
PlayoutId = playout.Id, |
||||
Index = add.Index, |
||||
TemplateId = add.TemplateId, |
||||
DaysOfWeek = add.DaysOfWeek, |
||||
DaysOfMonth = add.DaysOfMonth, |
||||
MonthsOfYear = add.MonthsOfYear |
||||
}); |
||||
} |
||||
|
||||
foreach (ReplacePlayoutTemplate update in toUpdate) |
||||
{ |
||||
foreach (PlayoutTemplate ex in existing.Filter(x => x.Id == update.Id)) |
||||
{ |
||||
ex.Index = update.Index; |
||||
ex.TemplateId = update.TemplateId; |
||||
ex.DaysOfWeek = update.DaysOfWeek; |
||||
ex.DaysOfMonth = update.DaysOfMonth; |
||||
ex.MonthsOfYear = update.MonthsOfYear; |
||||
} |
||||
} |
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken); |
||||
} |
||||
|
||||
return Option<BaseError>.None; |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
logger.LogError(ex, "Error saving playout template items"); |
||||
return BaseError.New(ex.Message); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ReplaceTemplateItem(int BlockId, TimeSpan StartTime); |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record ReplaceTemplateItems(int TemplateId, string Name, List<ReplaceTemplateItem> Items) |
||||
: IRequest<Either<BaseError, List<TemplateItemViewModel>>>; |
@ -0,0 +1,64 @@
@@ -0,0 +1,64 @@
|
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class ReplaceTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<ReplaceTemplateItems, Either<BaseError, List<TemplateItemViewModel>>> |
||||
{ |
||||
public async Task<Either<BaseError, List<TemplateItemViewModel>>> Handle( |
||||
ReplaceTemplateItems request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Template> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(ps => Persist(dbContext, request, ps)); |
||||
} |
||||
|
||||
private static async Task<List<TemplateItemViewModel>> Persist( |
||||
TvContext dbContext, |
||||
ReplaceTemplateItems request, |
||||
Template template) |
||||
{ |
||||
template.Name = request.Name; |
||||
|
||||
dbContext.RemoveRange(template.Items); |
||||
template.Items = request.Items.Map(i => BuildItem(template, i)).ToList(); |
||||
|
||||
await dbContext.SaveChangesAsync(); |
||||
|
||||
// TODO: refresh any playouts that use this schedule
|
||||
// foreach (Playout playout in programSchedule.Playouts)
|
||||
// {
|
||||
// await _channel.WriteAsync(new BuildPlayout(playout.Id, PlayoutBuildMode.Refresh));
|
||||
// }
|
||||
|
||||
await dbContext.Entry(template) |
||||
.Collection(t => t.Items) |
||||
.Query() |
||||
.Include(i => i.Block) |
||||
.LoadAsync(); |
||||
|
||||
return template.Items.Map(Mapper.ProjectToViewModel).ToList(); |
||||
} |
||||
|
||||
private static TemplateItem BuildItem(Template template, ReplaceTemplateItem item) => |
||||
new() |
||||
{ |
||||
TemplateId = template.Id, |
||||
BlockId = item.BlockId, |
||||
StartTime = item.StartTime |
||||
}; |
||||
|
||||
private static Task<Validation<BaseError, Template>> Validate(TvContext dbContext, ReplaceTemplateItems request) => |
||||
TemplateMustExist(dbContext, request.TemplateId); |
||||
|
||||
private static Task<Validation<BaseError, Template>> TemplateMustExist(TvContext dbContext, int templateId) => |
||||
dbContext.Templates |
||||
.Include(b => b.Items) |
||||
.SelectOneAsync(b => b.Id, b => b.Id == templateId) |
||||
.Map(o => o.ToValidation<BaseError>("[TemplateId] does not exist.")); |
||||
} |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
internal static class Mapper |
||||
{ |
||||
internal static BlockGroupViewModel ProjectToViewModel(BlockGroup blockGroup) => |
||||
new(blockGroup.Id, blockGroup.Name, blockGroup.Blocks.Count); |
||||
|
||||
internal static BlockViewModel ProjectToViewModel(Block block) => |
||||
new(block.Id, block.Name, block.Minutes); |
||||
|
||||
internal static BlockItemViewModel ProjectToViewModel(BlockItem blockItem) => |
||||
new( |
||||
blockItem.Id, |
||||
blockItem.Index, |
||||
blockItem.CollectionType, |
||||
blockItem.Collection is not null ? MediaCollections.Mapper.ProjectToViewModel(blockItem.Collection) : null, |
||||
blockItem.MultiCollection is not null |
||||
? MediaCollections.Mapper.ProjectToViewModel(blockItem.MultiCollection) |
||||
: null, |
||||
blockItem.SmartCollection is not null |
||||
? MediaCollections.Mapper.ProjectToViewModel(blockItem.SmartCollection) |
||||
: null, |
||||
blockItem.MediaItem switch |
||||
{ |
||||
Show show => MediaItems.Mapper.ProjectToViewModel(show), |
||||
Season season => MediaItems.Mapper.ProjectToViewModel(season), |
||||
Artist artist => MediaItems.Mapper.ProjectToViewModel(artist), |
||||
_ => null |
||||
}, |
||||
blockItem.PlaybackOrder); |
||||
|
||||
internal static TemplateGroupViewModel ProjectToViewModel(TemplateGroup templateGroup) => |
||||
new(templateGroup.Id, templateGroup.Name, templateGroup.Templates.Count); |
||||
|
||||
internal static TemplateViewModel ProjectToViewModel(Template template) => |
||||
new(template.Id, template.TemplateGroupId, template.Name); |
||||
|
||||
internal static TemplateItemViewModel ProjectToViewModel(TemplateItem templateItem) |
||||
{ |
||||
DateTime startTime = DateTime.Today.Add(templateItem.StartTime); |
||||
DateTime endTime = startTime.AddMinutes(templateItem.Block.Minutes); |
||||
return new TemplateItemViewModel(templateItem.BlockId, templateItem.Block.Name, startTime, endTime); |
||||
} |
||||
|
||||
internal static PlayoutTemplateViewModel ProjectToViewModel(PlayoutTemplate playoutTemplate) => |
||||
new( |
||||
playoutTemplate.Id, |
||||
ProjectToViewModel(playoutTemplate.Template), |
||||
playoutTemplate.Index, |
||||
playoutTemplate.DaysOfWeek, |
||||
playoutTemplate.DaysOfMonth, |
||||
playoutTemplate.MonthsOfYear); |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record PlayoutTemplateViewModel( |
||||
int Id, |
||||
TemplateViewModel Template, |
||||
int Index, |
||||
ICollection<DayOfWeek> DaysOfWeek, |
||||
ICollection<int> DaysOfMonth, |
||||
ICollection<int> MonthsOfYear); |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetAllBlockGroups : IRequest<List<BlockGroupViewModel>>; |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetAllBlockGroupsHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetAllBlockGroups, List<BlockGroupViewModel>> |
||||
{ |
||||
public async Task<List<BlockGroupViewModel>> Handle(GetAllBlockGroups request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
List<BlockGroup> blockGroups = await dbContext.BlockGroups |
||||
.AsNoTracking() |
||||
.Include(g => g.Blocks) |
||||
.ToListAsync(cancellationToken); |
||||
|
||||
return blockGroups.Map(Mapper.ProjectToViewModel).ToList(); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetAllTemplateGroups : IRequest<List<TemplateGroupViewModel>>; |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetAllTemplateGroupsHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetAllTemplateGroups, List<TemplateGroupViewModel>> |
||||
{ |
||||
public async Task<List<TemplateGroupViewModel>> Handle( |
||||
GetAllTemplateGroups request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
List<TemplateGroup> blockGroups = await dbContext.TemplateGroups |
||||
.AsNoTracking() |
||||
.Include(g => g.Templates) |
||||
.ToListAsync(cancellationToken); |
||||
|
||||
return blockGroups.Map(Mapper.ProjectToViewModel).ToList(); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetBlockById(int BlockId) : IRequest<Option<BlockViewModel>>; |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetBlockByIdHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetBlockById, Option<BlockViewModel>> |
||||
{ |
||||
public async Task<Option<BlockViewModel>> Handle(GetBlockById request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
return await dbContext.Blocks |
||||
.SelectOneAsync(b => b.Id, b => b.Id == request.BlockId) |
||||
.MapT(Mapper.ProjectToViewModel); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetBlockItems(int BlockId) : IRequest<List<BlockItemViewModel>>; |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetBlockItemsHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetBlockItems, List<BlockItemViewModel>> |
||||
{ |
||||
public async Task<List<BlockItemViewModel>> Handle(GetBlockItems request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
return await dbContext.BlockItems |
||||
.AsNoTracking() |
||||
.Filter(i => i.BlockId == request.BlockId) |
||||
.Include(i => i.Collection) |
||||
.Include(i => i.MultiCollection) |
||||
.Include(i => i.SmartCollection) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as Season).SeasonMetadata) |
||||
.ThenInclude(sm => sm.Artwork) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as Season).Show) |
||||
.ThenInclude(s => s.ShowMetadata) |
||||
.ThenInclude(sm => sm.Artwork) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as Show).ShowMetadata) |
||||
.ThenInclude(sm => sm.Artwork) |
||||
.Include(i => i.MediaItem) |
||||
.ThenInclude(i => (i as Artist).ArtistMetadata) |
||||
.ThenInclude(am => am.Artwork) |
||||
.ToListAsync(cancellationToken) |
||||
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList()); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetBlocksByBlockGroupId(int BlockGroupId) : IRequest<List<BlockViewModel>>; |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetBlocksByBlockGroupIdHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetBlocksByBlockGroupId, List<BlockViewModel>> |
||||
{ |
||||
public async Task<List<BlockViewModel>> Handle(GetBlocksByBlockGroupId request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
List<Block> blocks = await dbContext.Blocks |
||||
.Filter(b => b.BlockGroupId == request.BlockGroupId) |
||||
.AsNoTracking() |
||||
.ToListAsync(cancellationToken); |
||||
|
||||
return blocks.Map(Mapper.ProjectToViewModel).ToList(); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetPlayoutTemplates(int PlayoutId) : IRequest<List<PlayoutTemplateViewModel>>; |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetPlayoutTemplatesHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetPlayoutTemplates, List<PlayoutTemplateViewModel>> |
||||
{ |
||||
public async Task<List<PlayoutTemplateViewModel>> Handle( |
||||
GetPlayoutTemplates request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
List<PlayoutTemplate> playoutTemplates = await dbContext.PlayoutTemplates |
||||
.AsNoTracking() |
||||
.Filter(t => t.PlayoutId == request.PlayoutId) |
||||
.Include(t => t.Template) |
||||
.ToListAsync(cancellationToken); |
||||
|
||||
return playoutTemplates.Map(Mapper.ProjectToViewModel).ToList(); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetTemplateById(int TemplateId) : IRequest<Option<TemplateViewModel>>; |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetTemplateByIdHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetTemplateById, Option<TemplateViewModel>> |
||||
{ |
||||
public async Task<Option<TemplateViewModel>> Handle(GetTemplateById request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
return await dbContext.Templates |
||||
.SelectOneAsync(b => b.Id, b => b.Id == request.TemplateId) |
||||
.MapT(Mapper.ProjectToViewModel); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetTemplateItems(int TemplateId) : IRequest<List<TemplateItemViewModel>>; |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetTemplateItemsHandler(IDbContextFactory<TvContext> dbContextFactory) : IRequestHandler<GetTemplateItems, List<TemplateItemViewModel>> |
||||
{ |
||||
public async Task<List<TemplateItemViewModel>> Handle(GetTemplateItems request, CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
return await dbContext.TemplateItems |
||||
.AsNoTracking() |
||||
.Filter(i => i.TemplateId == request.TemplateId) |
||||
.Include(i => i.Block) |
||||
.ToListAsync(cancellationToken) |
||||
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList()); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record GetTemplatesByTemplateGroupId(int TemplateGroupId) : IRequest<List<TemplateViewModel>>; |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Infrastructure.Data; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public class GetTemplatesByTemplateGroupIdHandler(IDbContextFactory<TvContext> dbContextFactory) |
||||
: IRequestHandler<GetTemplatesByTemplateGroupId, List<TemplateViewModel>> |
||||
{ |
||||
public async Task<List<TemplateViewModel>> Handle( |
||||
GetTemplatesByTemplateGroupId request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
|
||||
return await dbContext.Templates |
||||
.AsNoTracking() |
||||
.Filter(i => i.TemplateGroupId == request.TemplateGroupId) |
||||
.ToListAsync(cancellationToken) |
||||
.Map(items => items.Map(Mapper.ProjectToViewModel).ToList()); |
||||
} |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record TemplateGroupViewModel(int Id, string Name, int TemplateCount); |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record TemplateItemViewModel(int BlockId, string BlockName, DateTime StartTime, DateTime EndTime); |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
namespace ErsatzTV.Application.Scheduling; |
||||
|
||||
public record TemplateViewModel(int Id, int TemplateGroupId, string Name); |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class Block |
||||
{ |
||||
public int Id { get; set; } |
||||
public int BlockGroupId { get; set; } |
||||
public BlockGroup BlockGroup { get; set; } |
||||
public string Name { get; set; } |
||||
public int Minutes { get; set; } |
||||
public ICollection<BlockItem> Items { get; set; } |
||||
public ICollection<TemplateItem> TemplateItems { get; set; } |
||||
public ICollection<PlayoutHistory> PlayoutHistory { get; set; } |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class BlockGroup |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Name { get; set; } |
||||
public ICollection<Block> Blocks { get; set; } |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class BlockItem |
||||
{ |
||||
public int Id { get; set; } |
||||
public int Index { get; set; } |
||||
public int BlockId { get; set; } |
||||
public Block Block { get; set; } |
||||
public ProgramScheduleItemCollectionType CollectionType { get; set; } |
||||
public int? CollectionId { get; set; } |
||||
public Collection Collection { get; set; } |
||||
public int? MediaItemId { get; set; } |
||||
public MediaItem MediaItem { get; set; } |
||||
public int? MultiCollectionId { get; set; } |
||||
public MultiCollection MultiCollection { get; set; } |
||||
public int? SmartCollectionId { get; set; } |
||||
public SmartCollection SmartCollection { get; set; } |
||||
public PlaybackOrder PlaybackOrder { get; set; } |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class PlayoutHistory |
||||
{ |
||||
public int Id { get; set; } |
||||
|
||||
public int PlayoutId { get; set; } |
||||
public Playout Playout { get; set; } |
||||
|
||||
public int BlockId { get; set; } |
||||
public Block Block { get; set; } |
||||
|
||||
// something that uniquely identifies the collection within the block
|
||||
public string Key { get; set; } |
||||
|
||||
// last occurence of an item from this collection in the playout
|
||||
public DateTime When { get; set; } |
||||
|
||||
// details about the item
|
||||
public string Details { get; set; } |
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class PlayoutTemplate |
||||
{ |
||||
public int Id { get; set; } |
||||
public int PlayoutId { get; set; } |
||||
public Playout Playout { get; set; } |
||||
public int TemplateId { get; set; } |
||||
public Template Template { get; set; } |
||||
public int Index { get; set; } |
||||
public ICollection<DayOfWeek> DaysOfWeek { get; set; } |
||||
public ICollection<int> DaysOfMonth { get; set; } |
||||
public ICollection<int> MonthsOfYear { get; set; } |
||||
public DateTimeOffset StartDate { get; set; } |
||||
public DateTimeOffset EndDate { get; set; } |
||||
|
||||
// TODO: ICollection<DateTimeOffset> AdditionalDays { get; set; }
|
||||
|
||||
public static List<DayOfWeek> AllDaysOfWeek() => |
||||
[ |
||||
DayOfWeek.Monday, |
||||
DayOfWeek.Tuesday, |
||||
DayOfWeek.Wednesday, |
||||
DayOfWeek.Thursday, |
||||
DayOfWeek.Friday, |
||||
DayOfWeek.Saturday, |
||||
DayOfWeek.Sunday |
||||
]; |
||||
|
||||
public static List<int> AllDaysOfMonth() => Enumerable.Range(1, 31).ToList(); |
||||
public static List<int> AllMonthsOfYear() => Enumerable.Range(1, 12).ToList(); |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class Template |
||||
{ |
||||
public int Id { get; set; } |
||||
public int TemplateGroupId { get; set; } |
||||
public TemplateGroup TemplateGroup { get; set; } |
||||
public string Name { get; set; } |
||||
public ICollection<TemplateItem> Items { get; set; } |
||||
public ICollection<PlayoutTemplate> PlayoutTemplates { get; set; } |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class TemplateGroup |
||||
{ |
||||
public int Id { get; set; } |
||||
public string Name { get; set; } |
||||
public ICollection<Template> Templates { get; set; } |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
namespace ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
public class TemplateItem |
||||
{ |
||||
public int Id { get; set; } |
||||
public int TemplateId { get; set; } |
||||
public Template Template { get; set; } |
||||
public int BlockId { get; set; } |
||||
public Block Block { get; set; } |
||||
public TimeSpan StartTime { get; set; } |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
public interface IBlockPlayoutBuilder |
||||
{ |
||||
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken); |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
public interface IExternalJsonPlayoutBuilder |
||||
{ |
||||
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken); |
||||
} |
@ -0,0 +1,367 @@
@@ -0,0 +1,367 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.Extensions; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using Microsoft.Extensions.Logging; |
||||
using Newtonsoft.Json; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling; |
||||
|
||||
public class BlockPlayoutBuilder( |
||||
IConfigElementRepository configElementRepository, |
||||
IMediaCollectionRepository mediaCollectionRepository, |
||||
ITelevisionRepository televisionRepository, |
||||
IArtistRepository artistRepository, |
||||
ILogger<BlockPlayoutBuilder> logger) |
||||
: IBlockPlayoutBuilder |
||||
{ |
||||
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) |
||||
{ |
||||
logger.LogDebug( |
||||
"Building block playout {PlayoutId} for channel {ChannelNumber} - {ChannelName}", |
||||
playout.Id, |
||||
playout.Channel.Number, |
||||
playout.Channel.Name); |
||||
|
||||
DateTimeOffset start = DateTimeOffset.Now; |
||||
|
||||
// get blocks to schedule
|
||||
List<RealBlock> blocksToSchedule = await GetBlocksToSchedule(playout, start); |
||||
|
||||
// get all collection items for the playout
|
||||
Map<CollectionKey, List<MediaItem>> collectionMediaItems = await GetCollectionMediaItems(blocksToSchedule); |
||||
|
||||
// TODO: REMOVE THIS !!!
|
||||
playout.Items.Clear(); |
||||
|
||||
// TODO: REMOVE THIS !!!
|
||||
var historyToRemove = playout.PlayoutHistory |
||||
.Filter(h => h.When > start.UtcDateTime) |
||||
.ToList(); |
||||
foreach (PlayoutHistory remove in historyToRemove) |
||||
{ |
||||
playout.PlayoutHistory.Remove(remove); |
||||
} |
||||
|
||||
foreach (RealBlock realBlock in blocksToSchedule) |
||||
{ |
||||
logger.LogDebug( |
||||
"Will schedule block {Block} at {Start}", |
||||
realBlock.Block.Name, |
||||
realBlock.Start); |
||||
|
||||
DateTimeOffset currentTime = realBlock.Start; |
||||
|
||||
foreach (BlockItem blockItem in realBlock.Block.Items) |
||||
{ |
||||
// TODO: support other playback orders
|
||||
if (blockItem.PlaybackOrder is not PlaybackOrder.SeasonEpisode and not PlaybackOrder.Chronological) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
// TODO: check if change is needed - if not, skip building
|
||||
// - block can change
|
||||
// - template can change
|
||||
// - playout templates can change
|
||||
|
||||
// TODO: check for playout history for this collection
|
||||
string historyKey = HistoryKey.ForBlockItem(blockItem); |
||||
logger.LogDebug("History key for block item {Item} is {Key}", blockItem.Id, historyKey); |
||||
|
||||
DateTime historyTime = currentTime.UtcDateTime; |
||||
Option<PlayoutHistory> maybeHistory = playout.PlayoutHistory |
||||
.Filter(h => h.BlockId == blockItem.BlockId) |
||||
.Filter(h => h.Key == historyKey) |
||||
.Filter(h => h.When < historyTime) |
||||
.OrderByDescending(h => h.When) |
||||
.HeadOrNone(); |
||||
|
||||
var state = new CollectionEnumeratorState { Seed = 0, Index = 0 }; |
||||
|
||||
var collectionKey = CollectionKey.ForBlockItem(blockItem); |
||||
List<MediaItem> collectionItems = collectionMediaItems[collectionKey]; |
||||
// get enumerator
|
||||
var enumerator = new SeasonEpisodeMediaCollectionEnumerator(collectionItems, state); |
||||
|
||||
// seek to the appropriate place in the collection enumerator
|
||||
foreach (PlayoutHistory history in maybeHistory) |
||||
{ |
||||
logger.LogDebug("History is applicable: {When}: {History}", history.When, history.Details); |
||||
|
||||
// find next media item
|
||||
HistoryDetails.Details details = JsonConvert.DeserializeObject<HistoryDetails.Details>(history.Details); |
||||
if (details.SeasonNumber.HasValue && details.EpisodeNumber.HasValue) |
||||
{ |
||||
Option<MediaItem> maybeMatchedItem = Optional( |
||||
collectionItems.Find( |
||||
ci => ci is Episode e && |
||||
e.EpisodeMetadata.Any(em => em.EpisodeNumber == details.EpisodeNumber.Value) && |
||||
e.Season.SeasonNumber == details.SeasonNumber.Value)); |
||||
|
||||
var copy = collectionItems.ToList(); |
||||
|
||||
if (maybeMatchedItem.IsNone) |
||||
{ |
||||
var fakeItem = new Episode |
||||
{ |
||||
Season = new Season { SeasonNumber = details.SeasonNumber.Value }, |
||||
EpisodeMetadata = |
||||
[ |
||||
new EpisodeMetadata |
||||
{ |
||||
EpisodeNumber = details.EpisodeNumber.Value, |
||||
ReleaseDate = details.ReleaseDate |
||||
} |
||||
] |
||||
}; |
||||
|
||||
copy.Add(fakeItem); |
||||
maybeMatchedItem = fakeItem; |
||||
} |
||||
|
||||
foreach (MediaItem matchedItem in maybeMatchedItem) |
||||
{ |
||||
IComparer<MediaItem> comparer = blockItem.PlaybackOrder switch |
||||
{ |
||||
PlaybackOrder.Chronological => new ChronologicalMediaComparer(), |
||||
_ => new SeasonEpisodeMediaComparer() |
||||
}; |
||||
|
||||
copy.Sort(comparer); |
||||
|
||||
state.Index = copy.IndexOf(matchedItem); |
||||
enumerator.ResetState(state); |
||||
enumerator.MoveNext(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
foreach (MediaItem mediaItem in enumerator.Current) |
||||
{ |
||||
logger.LogDebug("current item: {Id} / {Title}", mediaItem.Id, mediaItem is Episode e ? GetTitle(e) : string.Empty); |
||||
|
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem); |
||||
|
||||
// TODO: create a playout item
|
||||
var playoutItem = new PlayoutItem |
||||
{ |
||||
MediaItemId = mediaItem.Id, |
||||
Start = currentTime.UtcDateTime, |
||||
Finish = currentTime.UtcDateTime + itemDuration, |
||||
InPoint = TimeSpan.Zero, |
||||
OutPoint = itemDuration, |
||||
FillerKind = FillerKind.None, |
||||
//CustomTitle = scheduleItem.CustomTitle,
|
||||
//WatermarkId = scheduleItem.WatermarkId,
|
||||
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
//SubtitleMode = scheduleItem.SubtitleMode
|
||||
}; |
||||
|
||||
playout.Items.Add(playoutItem); |
||||
|
||||
// TODO: create a playout history record
|
||||
var nextHistory = new PlayoutHistory |
||||
{ |
||||
PlayoutId = playout.Id, |
||||
BlockId = blockItem.BlockId, |
||||
When = currentTime.UtcDateTime, |
||||
Key = historyKey, |
||||
Details = HistoryDetails.ForMediaItem(mediaItem) |
||||
}; |
||||
|
||||
playout.PlayoutHistory.Add(nextHistory); |
||||
|
||||
currentTime += itemDuration; |
||||
enumerator.MoveNext(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
CleanUpHistory(playout, start); |
||||
|
||||
return playout; |
||||
} |
||||
|
||||
private static string GetTitle(Episode e) |
||||
{ |
||||
string showTitle = e.Season.Show.ShowMetadata.HeadOrNone() |
||||
.Map(sm => $"{sm.Title} - ").IfNone(string.Empty); |
||||
var episodeNumbers = e.EpisodeMetadata.Map(em => em.EpisodeNumber).ToList(); |
||||
var episodeTitles = e.EpisodeMetadata.Map(em => em.Title).ToList(); |
||||
if (episodeNumbers.Count == 0 || episodeTitles.Count == 0) |
||||
{ |
||||
return "[unknown episode]"; |
||||
} |
||||
|
||||
var numbersString = $"e{string.Join('e', episodeNumbers.Map(n => $"{n:00}"))}"; |
||||
var titlesString = $"{string.Join('/', episodeTitles)}"; |
||||
|
||||
return $"{showTitle}s{e.Season.SeasonNumber:00}{numbersString} - {titlesString}"; |
||||
} |
||||
|
||||
private async Task<List<RealBlock>> GetBlocksToSchedule(Playout playout, DateTimeOffset start) |
||||
{ |
||||
int daysToBuild = await configElementRepository.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild) |
||||
.IfNoneAsync(2); |
||||
|
||||
DateTimeOffset finish = start.AddDays(daysToBuild); |
||||
|
||||
var realBlocks = new List<RealBlock>(); |
||||
DateTimeOffset current = start.Date; |
||||
while (current < finish) |
||||
{ |
||||
foreach (PlayoutTemplate playoutTemplate in PlayoutTemplateSelector.GetPlayoutTemplateFor(playout.Templates, current)) |
||||
{ |
||||
// logger.LogDebug(
|
||||
// "Will schedule day {Date} using template {Template}",
|
||||
// current,
|
||||
// playoutTemplate.Template.Name);
|
||||
|
||||
foreach (TemplateItem templateItem in playoutTemplate.Template.Items) |
||||
{ |
||||
var realBlock = new RealBlock( |
||||
templateItem.Block, |
||||
new DateTimeOffset( |
||||
current.Year, |
||||
current.Month, |
||||
current.Day, |
||||
templateItem.StartTime.Hours, |
||||
templateItem.StartTime.Minutes, |
||||
0, |
||||
start.Offset)); |
||||
|
||||
realBlocks.Add(realBlock); |
||||
} |
||||
|
||||
current = current.AddDays(1); |
||||
} |
||||
} |
||||
|
||||
realBlocks.RemoveAll(b => b.Start.AddMinutes(b.Block.Minutes) < start || b.Start > finish); |
||||
|
||||
return realBlocks; |
||||
} |
||||
|
||||
private void CleanUpHistory(Playout playout, DateTimeOffset start) |
||||
{ |
||||
var groups = new Dictionary<string, List<PlayoutHistory>>(); |
||||
foreach (PlayoutHistory history in playout.PlayoutHistory) |
||||
{ |
||||
var key = $"{history.BlockId}-{history.Key}"; |
||||
if (!groups.TryGetValue(key, out List<PlayoutHistory> group)) |
||||
{ |
||||
group = []; |
||||
groups[key] = group; |
||||
} |
||||
|
||||
group.Add(history); |
||||
} |
||||
|
||||
foreach ((string key, List<PlayoutHistory> group) in groups) |
||||
{ |
||||
logger.LogDebug("History key {Key} has {Count} items in group", key, group.Count); |
||||
|
||||
IEnumerable<PlayoutHistory> toDelete = group |
||||
.Filter(h => h.When < start.UtcDateTime) |
||||
.OrderByDescending(h => h.When) |
||||
.Tail(); |
||||
|
||||
foreach (PlayoutHistory delete in toDelete) |
||||
{ |
||||
playout.PlayoutHistory.Remove(delete); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private async Task<Map<CollectionKey, List<MediaItem>>> GetCollectionMediaItems(List<RealBlock> realBlocks) |
||||
{ |
||||
var collectionKeys = realBlocks.Map(b => b.Block.Items) |
||||
.Flatten() |
||||
.DistinctBy(i => i.Id) |
||||
.Map(CollectionKey.ForBlockItem) |
||||
.Distinct() |
||||
.ToList(); |
||||
|
||||
IEnumerable<Tuple<CollectionKey, List<MediaItem>>> tuples = await collectionKeys.Map( |
||||
async collectionKey => Tuple( |
||||
collectionKey, |
||||
await MediaItemsForCollection.Collect( |
||||
mediaCollectionRepository, |
||||
televisionRepository, |
||||
artistRepository, |
||||
collectionKey))).SequenceParallel(); |
||||
|
||||
return LanguageExt.Map.createRange(tuples); |
||||
} |
||||
|
||||
private static TimeSpan DurationForMediaItem(MediaItem mediaItem) |
||||
{ |
||||
MediaVersion version = mediaItem.GetHeadVersion(); |
||||
return version.Duration; |
||||
} |
||||
|
||||
private record RealBlock(Block Block, DateTimeOffset Start); |
||||
|
||||
private static class HistoryKey |
||||
{ |
||||
private static readonly JsonSerializerSettings Settings = new() |
||||
{ |
||||
NullValueHandling = NullValueHandling.Ignore |
||||
}; |
||||
|
||||
public static string ForBlockItem(BlockItem blockItem) |
||||
{ |
||||
dynamic key = new |
||||
{ |
||||
blockItem.BlockId, |
||||
blockItem.PlaybackOrder, |
||||
blockItem.CollectionType, |
||||
blockItem.CollectionId, |
||||
blockItem.MultiCollectionId, |
||||
blockItem.SmartCollectionId, |
||||
blockItem.MediaItemId |
||||
}; |
||||
|
||||
return JsonConvert.SerializeObject(key, Formatting.None, Settings); |
||||
} |
||||
} |
||||
|
||||
private static class HistoryDetails |
||||
{ |
||||
private static readonly JsonSerializerSettings Settings = new() |
||||
{ |
||||
NullValueHandling = NullValueHandling.Ignore |
||||
}; |
||||
|
||||
public static string ForMediaItem(MediaItem mediaItem) |
||||
{ |
||||
Details details = mediaItem switch |
||||
{ |
||||
Episode e => ForEpisode(e), |
||||
_ => new Details(mediaItem.Id, null, null, null) |
||||
}; |
||||
|
||||
return JsonConvert.SerializeObject(details, Formatting.None, Settings); |
||||
} |
||||
|
||||
private static Details ForEpisode(Episode e) |
||||
{ |
||||
int? episodeNumber = null; |
||||
DateTime? releaseDate = null; |
||||
foreach (EpisodeMetadata episodeMetadata in e.EpisodeMetadata.HeadOrNone()) |
||||
{ |
||||
episodeNumber = episodeMetadata.EpisodeNumber; |
||||
releaseDate = episodeMetadata.ReleaseDate; |
||||
} |
||||
|
||||
return new Details(e.Id, releaseDate, e.Season.SeasonNumber, episodeNumber); |
||||
} |
||||
|
||||
public record Details(int? MediaItemId, DateTime? ReleaseDate, int? SeasonNumber, int? EpisodeNumber); |
||||
} |
||||
} |
@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling; |
||||
|
||||
public class ExternalJsonPlayoutBuilder(ILogger<ExternalJsonPlayoutBuilder> logger) : IExternalJsonPlayoutBuilder |
||||
{ |
||||
// nothing to do for external json playouts
|
||||
public Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) |
||||
{ |
||||
logger.LogDebug( |
||||
"Building external json playout for channel {Number} - {Name}", |
||||
playout.Channel.Number, |
||||
playout.Channel.Name); |
||||
|
||||
return Task.FromResult(playout); |
||||
} |
||||
} |
@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling; |
||||
|
||||
public static class PlayoutTemplateSelector |
||||
{ |
||||
public static Option<PlayoutTemplate> GetPlayoutTemplateFor( |
||||
IEnumerable<PlayoutTemplate> templates, |
||||
DateTimeOffset date) |
||||
{ |
||||
foreach (PlayoutTemplate template in templates.OrderBy(x => x.Index)) |
||||
{ |
||||
bool daysOfWeek = template.DaysOfWeek.Contains(date.DayOfWeek); |
||||
if (!daysOfWeek) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
bool daysOfMonth = template.DaysOfMonth.Contains(date.Day); |
||||
if (!daysOfMonth) |
||||
{ |
||||
continue; |
||||
} |
||||
|
||||
bool monthOfYear = template.MonthsOfYear.Contains(date.Month); |
||||
if (monthOfYear) |
||||
{ |
||||
return template; |
||||
} |
||||
} |
||||
|
||||
return Option<PlayoutTemplate>.None; |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,355 @@
@@ -0,0 +1,355 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Metadata; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_BlockScheduling : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.CreateTable( |
||||
name: "BlockGroup", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4") |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_BlockGroup", x => x.Id); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "TemplateGroup", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4") |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_TemplateGroup", x => x.Id); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "Block", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
BlockGroupId = table.Column<int>(type: "int", nullable: false), |
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
Minutes = table.Column<int>(type: "int", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_Block", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_Block_BlockGroup_BlockGroupId", |
||||
column: x => x.BlockGroupId, |
||||
principalTable: "BlockGroup", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "Template", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
TemplateGroupId = table.Column<int>(type: "int", nullable: false), |
||||
Name = table.Column<string>(type: "varchar(255)", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4") |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_Template", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_Template_TemplateGroup_TemplateGroupId", |
||||
column: x => x.TemplateGroupId, |
||||
principalTable: "TemplateGroup", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "BlockItem", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
Index = table.Column<int>(type: "int", nullable: false), |
||||
BlockId = table.Column<int>(type: "int", nullable: false), |
||||
CollectionType = table.Column<int>(type: "int", nullable: false), |
||||
CollectionId = table.Column<int>(type: "int", nullable: true), |
||||
MediaItemId = table.Column<int>(type: "int", nullable: true), |
||||
MultiCollectionId = table.Column<int>(type: "int", nullable: true), |
||||
SmartCollectionId = table.Column<int>(type: "int", nullable: true), |
||||
PlaybackOrder = table.Column<int>(type: "int", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_BlockItem", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_Block_BlockId", |
||||
column: x => x.BlockId, |
||||
principalTable: "Block", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_Collection_CollectionId", |
||||
column: x => x.CollectionId, |
||||
principalTable: "Collection", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_MediaItem_MediaItemId", |
||||
column: x => x.MediaItemId, |
||||
principalTable: "MediaItem", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_MultiCollection_MultiCollectionId", |
||||
column: x => x.MultiCollectionId, |
||||
principalTable: "MultiCollection", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_SmartCollection_SmartCollectionId", |
||||
column: x => x.SmartCollectionId, |
||||
principalTable: "SmartCollection", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "PlayoutHistory", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
PlayoutId = table.Column<int>(type: "int", nullable: false), |
||||
BlockId = table.Column<int>(type: "int", nullable: false), |
||||
Key = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
When = table.Column<DateTime>(type: "datetime(6)", nullable: false), |
||||
Details = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4") |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_PlayoutHistory", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutHistory_Block_BlockId", |
||||
column: x => x.BlockId, |
||||
principalTable: "Block", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutHistory_Playout_PlayoutId", |
||||
column: x => x.PlayoutId, |
||||
principalTable: "Playout", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "PlayoutTemplate", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
PlayoutId = table.Column<int>(type: "int", nullable: false), |
||||
TemplateId = table.Column<int>(type: "int", nullable: false), |
||||
Index = table.Column<int>(type: "int", nullable: false), |
||||
DaysOfWeek = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
DaysOfMonth = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
MonthsOfYear = table.Column<string>(type: "longtext", nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"), |
||||
StartDate = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false), |
||||
EndDate = table.Column<DateTimeOffset>(type: "datetime(6)", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_PlayoutTemplate", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutTemplate_Playout_PlayoutId", |
||||
column: x => x.PlayoutId, |
||||
principalTable: "Playout", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutTemplate_Template_TemplateId", |
||||
column: x => x.TemplateId, |
||||
principalTable: "Template", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "TemplateItem", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "int", nullable: false) |
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), |
||||
TemplateId = table.Column<int>(type: "int", nullable: false), |
||||
BlockId = table.Column<int>(type: "int", nullable: false), |
||||
StartTime = table.Column<TimeSpan>(type: "time(6)", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_TemplateItem", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_TemplateItem_Block_BlockId", |
||||
column: x => x.BlockId, |
||||
principalTable: "Block", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_TemplateItem_Template_TemplateId", |
||||
column: x => x.TemplateId, |
||||
principalTable: "Template", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Block_BlockGroupId", |
||||
table: "Block", |
||||
column: "BlockGroupId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Block_Name", |
||||
table: "Block", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockGroup_Name", |
||||
table: "BlockGroup", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_BlockId", |
||||
table: "BlockItem", |
||||
column: "BlockId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_CollectionId", |
||||
table: "BlockItem", |
||||
column: "CollectionId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_MediaItemId", |
||||
table: "BlockItem", |
||||
column: "MediaItemId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_MultiCollectionId", |
||||
table: "BlockItem", |
||||
column: "MultiCollectionId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_SmartCollectionId", |
||||
table: "BlockItem", |
||||
column: "SmartCollectionId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutHistory_BlockId", |
||||
table: "PlayoutHistory", |
||||
column: "BlockId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutHistory_PlayoutId", |
||||
table: "PlayoutHistory", |
||||
column: "PlayoutId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutTemplate_PlayoutId", |
||||
table: "PlayoutTemplate", |
||||
column: "PlayoutId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutTemplate_TemplateId", |
||||
table: "PlayoutTemplate", |
||||
column: "TemplateId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Template_Name", |
||||
table: "Template", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Template_TemplateGroupId", |
||||
table: "Template", |
||||
column: "TemplateGroupId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_TemplateGroup_Name", |
||||
table: "TemplateGroup", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_TemplateItem_BlockId", |
||||
table: "TemplateItem", |
||||
column: "BlockId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_TemplateItem_TemplateId", |
||||
table: "TemplateItem", |
||||
column: "TemplateId"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "BlockItem"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "PlayoutHistory"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "PlayoutTemplate"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "TemplateItem"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "Block"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "Template"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "BlockGroup"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "TemplateGroup"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,153 @@
@@ -0,0 +1,153 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_Block : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.CreateTable( |
||||
name: "BlockGroup", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
Name = table.Column<string>(type: "TEXT", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_BlockGroup", x => x.Id); |
||||
}); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "Block", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
BlockGroupId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
Name = table.Column<string>(type: "TEXT", nullable: true), |
||||
Minutes = table.Column<int>(type: "INTEGER", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_Block", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_Block_BlockGroup_BlockGroupId", |
||||
column: x => x.BlockGroupId, |
||||
principalTable: "BlockGroup", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "BlockItem", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
Index = table.Column<int>(type: "INTEGER", nullable: false), |
||||
BlockId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
CollectionType = table.Column<int>(type: "INTEGER", nullable: false), |
||||
CollectionId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
MediaItemId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
MultiCollectionId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
SmartCollectionId = table.Column<int>(type: "INTEGER", nullable: true), |
||||
PlaybackOrder = table.Column<int>(type: "INTEGER", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_BlockItem", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_Block_BlockId", |
||||
column: x => x.BlockId, |
||||
principalTable: "Block", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_Collection_CollectionId", |
||||
column: x => x.CollectionId, |
||||
principalTable: "Collection", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_MediaItem_MediaItemId", |
||||
column: x => x.MediaItemId, |
||||
principalTable: "MediaItem", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_MultiCollection_MultiCollectionId", |
||||
column: x => x.MultiCollectionId, |
||||
principalTable: "MultiCollection", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_BlockItem_SmartCollection_SmartCollectionId", |
||||
column: x => x.SmartCollectionId, |
||||
principalTable: "SmartCollection", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Block_BlockGroupId", |
||||
table: "Block", |
||||
column: "BlockGroupId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Block_Name", |
||||
table: "Block", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockGroup_Name", |
||||
table: "BlockGroup", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_BlockId", |
||||
table: "BlockItem", |
||||
column: "BlockId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_CollectionId", |
||||
table: "BlockItem", |
||||
column: "CollectionId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_MediaItemId", |
||||
table: "BlockItem", |
||||
column: "MediaItemId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_MultiCollectionId", |
||||
table: "BlockItem", |
||||
column: "MultiCollectionId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_BlockItem_SmartCollectionId", |
||||
table: "BlockItem", |
||||
column: "SmartCollectionId"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "BlockItem"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "Block"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "BlockGroup"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_Template : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.CreateTable( |
||||
name: "TemplateGroup", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
Name = table.Column<string>(type: "TEXT", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_TemplateGroup", x => x.Id); |
||||
}); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "Template", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
TemplateGroupId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
Name = table.Column<string>(type: "TEXT", nullable: true), |
||||
PlayoutId = table.Column<int>(type: "INTEGER", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_Template", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_Template_Playout_PlayoutId", |
||||
column: x => x.PlayoutId, |
||||
principalTable: "Playout", |
||||
principalColumn: "Id"); |
||||
table.ForeignKey( |
||||
name: "FK_Template_TemplateGroup_TemplateGroupId", |
||||
column: x => x.TemplateGroupId, |
||||
principalTable: "TemplateGroup", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "TemplateItem", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
TemplateId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
BlockId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
StartTime = table.Column<TimeSpan>(type: "TEXT", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_TemplateItem", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_TemplateItem_Block_BlockId", |
||||
column: x => x.BlockId, |
||||
principalTable: "Block", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_TemplateItem_Template_TemplateId", |
||||
column: x => x.TemplateId, |
||||
principalTable: "Template", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Template_Name", |
||||
table: "Template", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Template_PlayoutId", |
||||
table: "Template", |
||||
column: "PlayoutId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Template_TemplateGroupId", |
||||
table: "Template", |
||||
column: "TemplateGroupId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_TemplateGroup_Name", |
||||
table: "TemplateGroup", |
||||
column: "Name", |
||||
unique: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_TemplateItem_BlockId", |
||||
table: "TemplateItem", |
||||
column: "BlockId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_TemplateItem_TemplateId", |
||||
table: "TemplateItem", |
||||
column: "TemplateId"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "TemplateItem"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "Template"); |
||||
|
||||
migrationBuilder.DropTable( |
||||
name: "TemplateGroup"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutTemplate : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropForeignKey( |
||||
name: "FK_Template_Playout_PlayoutId", |
||||
table: "Template"); |
||||
|
||||
migrationBuilder.DropIndex( |
||||
name: "IX_Template_PlayoutId", |
||||
table: "Template"); |
||||
|
||||
migrationBuilder.DropColumn( |
||||
name: "PlayoutId", |
||||
table: "Template"); |
||||
|
||||
migrationBuilder.CreateTable( |
||||
name: "PlayoutTemplate", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
PlayoutId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
TemplateId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
Index = table.Column<int>(type: "INTEGER", nullable: false), |
||||
DaysOfWeek = table.Column<string>(type: "TEXT", nullable: true), |
||||
MonthsOfYear = table.Column<string>(type: "TEXT", nullable: true), |
||||
StartDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: false), |
||||
EndDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: false) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_PlayoutTemplate", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutTemplate_Playout_PlayoutId", |
||||
column: x => x.PlayoutId, |
||||
principalTable: "Playout", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutTemplate_Template_TemplateId", |
||||
column: x => x.TemplateId, |
||||
principalTable: "Template", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutTemplate_PlayoutId", |
||||
table: "PlayoutTemplate", |
||||
column: "PlayoutId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutTemplate_TemplateId", |
||||
table: "PlayoutTemplate", |
||||
column: "TemplateId"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "PlayoutTemplate"); |
||||
|
||||
migrationBuilder.AddColumn<int>( |
||||
name: "PlayoutId", |
||||
table: "Template", |
||||
type: "INTEGER", |
||||
nullable: true); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_Template_PlayoutId", |
||||
table: "Template", |
||||
column: "PlayoutId"); |
||||
|
||||
migrationBuilder.AddForeignKey( |
||||
name: "FK_Template_Playout_PlayoutId", |
||||
table: "Template", |
||||
column: "PlayoutId", |
||||
principalTable: "Playout", |
||||
principalColumn: "Id"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutTemplate_DaysOfMonth : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "DaysOfMonth", |
||||
table: "PlayoutTemplate", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "DaysOfMonth", |
||||
table: "PlayoutTemplate"); |
||||
} |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
using System; |
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutHistory : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.CreateTable( |
||||
name: "PlayoutHistory", |
||||
columns: table => new |
||||
{ |
||||
Id = table.Column<int>(type: "INTEGER", nullable: false) |
||||
.Annotation("Sqlite:Autoincrement", true), |
||||
PlayoutId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
BlockId = table.Column<int>(type: "INTEGER", nullable: false), |
||||
Key = table.Column<string>(type: "TEXT", nullable: true), |
||||
When = table.Column<DateTimeOffset>(type: "TEXT", nullable: false), |
||||
Details = table.Column<string>(type: "TEXT", nullable: true) |
||||
}, |
||||
constraints: table => |
||||
{ |
||||
table.PrimaryKey("PK_PlayoutHistory", x => x.Id); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutHistory_Block_BlockId", |
||||
column: x => x.BlockId, |
||||
principalTable: "Block", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
table.ForeignKey( |
||||
name: "FK_PlayoutHistory_Playout_PlayoutId", |
||||
column: x => x.PlayoutId, |
||||
principalTable: "Playout", |
||||
principalColumn: "Id", |
||||
onDelete: ReferentialAction.Cascade); |
||||
}); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutHistory_BlockId", |
||||
table: "PlayoutHistory", |
||||
column: "BlockId"); |
||||
|
||||
migrationBuilder.CreateIndex( |
||||
name: "IX_PlayoutHistory_PlayoutId", |
||||
table: "PlayoutHistory", |
||||
column: "PlayoutId"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropTable( |
||||
name: "PlayoutHistory"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling; |
||||
|
||||
public class BlockConfiguration : IEntityTypeConfiguration<Block> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<Block> builder) |
||||
{ |
||||
builder.ToTable("Block"); |
||||
|
||||
builder.HasIndex(b => b.Name) |
||||
.IsUnique(); |
||||
|
||||
builder.HasMany(b => b.Items) |
||||
.WithOne(i => i.Block) |
||||
.HasForeignKey(i => i.BlockId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(b => b.TemplateItems) |
||||
.WithOne(i => i.Block) |
||||
.HasForeignKey(i => i.BlockId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(b => b.PlayoutHistory) |
||||
.WithOne(h => h.Block) |
||||
.HasForeignKey(h => h.BlockId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
} |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling; |
||||
|
||||
public class BlockGroupConfiguration : IEntityTypeConfiguration<BlockGroup> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<BlockGroup> builder) |
||||
{ |
||||
builder.ToTable("BlockGroup"); |
||||
|
||||
builder.HasIndex(b => b.Name) |
||||
.IsUnique(); |
||||
|
||||
builder.HasMany(b => b.Blocks) |
||||
.WithOne(i => i.BlockGroup) |
||||
.HasForeignKey(i => i.BlockGroupId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
} |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling; |
||||
|
||||
public class BlockItemConfiguration : IEntityTypeConfiguration<BlockItem> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<BlockItem> builder) |
||||
{ |
||||
builder.ToTable("BlockItem"); |
||||
|
||||
builder.HasOne(i => i.Collection) |
||||
.WithMany() |
||||
.HasForeignKey(i => i.CollectionId) |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(false); |
||||
|
||||
builder.HasOne(i => i.MediaItem) |
||||
.WithMany() |
||||
.HasForeignKey(i => i.MediaItemId) |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(false); |
||||
|
||||
builder.HasOne(i => i.MultiCollection) |
||||
.WithMany() |
||||
.HasForeignKey(i => i.MultiCollectionId) |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(false); |
||||
|
||||
builder.HasOne(i => i.SmartCollection) |
||||
.WithMany() |
||||
.HasForeignKey(i => i.SmartCollectionId) |
||||
.OnDelete(DeleteBehavior.Cascade) |
||||
.IsRequired(false); |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling; |
||||
|
||||
public class PlayoutHistoryConfiguration : IEntityTypeConfiguration<PlayoutHistory> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<PlayoutHistory> builder) |
||||
{ |
||||
builder.ToTable("PlayoutHistory"); |
||||
} |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling; |
||||
|
||||
public class PlayoutTemplateConfiguration : IEntityTypeConfiguration<PlayoutTemplate> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<PlayoutTemplate> builder) |
||||
{ |
||||
builder.ToTable("PlayoutTemplate"); |
||||
|
||||
builder.Property(t => t.DaysOfMonth) |
||||
.HasConversion<IntCollectionValueConverter, CollectionValueComparer<int>>(); |
||||
|
||||
builder.Property(t => t.MonthsOfYear) |
||||
.HasConversion<IntCollectionValueConverter, CollectionValueComparer<int>>(); |
||||
|
||||
builder.Property(t => t.DaysOfWeek) |
||||
.HasConversion<EnumCollectionJsonValueConverter<DayOfWeek>, CollectionValueComparer<DayOfWeek>>(); |
||||
} |
||||
} |
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling; |
||||
|
||||
public class TemplateConfiguration : IEntityTypeConfiguration<Template> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<Template> builder) |
||||
{ |
||||
builder.ToTable("Template"); |
||||
|
||||
builder.HasIndex(b => b.Name) |
||||
.IsUnique(); |
||||
|
||||
builder.HasMany(b => b.Items) |
||||
.WithOne(i => i.Template) |
||||
.HasForeignKey(i => i.TemplateId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
|
||||
builder.HasMany(t => t.PlayoutTemplates) |
||||
.WithOne(t => t.Template) |
||||
.HasForeignKey(t => t.TemplateId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
} |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using Microsoft.EntityFrameworkCore; |
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders; |
||||
|
||||
namespace ErsatzTV.Infrastructure.Data.Configurations.Scheduling; |
||||
|
||||
public class TemplateGroupConfiguration : IEntityTypeConfiguration<TemplateGroup> |
||||
{ |
||||
public void Configure(EntityTypeBuilder<TemplateGroup> builder) |
||||
{ |
||||
builder.ToTable("TemplateGroup"); |
||||
|
||||
builder.HasIndex(b => b.Name) |
||||
.IsUnique(); |
||||
|
||||
builder.HasMany(b => b.Templates) |
||||
.WithOne(i => i.TemplateGroup) |
||||
.HasForeignKey(i => i.TemplateGroupId) |
||||
.OnDelete(DeleteBehavior.Cascade); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue