mirror of https://github.com/ErsatzTV/ErsatzTV.git
Browse Source
* add template playout kind * add template scheduler count * implement pad to next * only allow resetting template playouts * update changelogpull/1809/head
42 changed files with 12371 additions and 68 deletions
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application.Channels; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
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 CreateTemplatePlayoutHandler |
||||
: IRequestHandler<CreateTemplatePlayout, Either<BaseError, CreatePlayoutResponse>> |
||||
{ |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly ILocalFileSystem _localFileSystem; |
||||
|
||||
public CreateTemplatePlayoutHandler( |
||||
ILocalFileSystem localFileSystem, |
||||
ChannelWriter<IBackgroundServiceRequest> channel, |
||||
IDbContextFactory<TvContext> dbContextFactory) |
||||
{ |
||||
_localFileSystem = localFileSystem; |
||||
_channel = channel; |
||||
_dbContextFactory = dbContextFactory; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle( |
||||
CreateTemplatePlayout 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 async Task<Validation<BaseError, Playout>> Validate( |
||||
TvContext dbContext, |
||||
CreateTemplatePlayout request) => |
||||
(await ValidateChannel(dbContext, request), ValidateTemplateFile(request), ValidatePlayoutType(request)) |
||||
.Apply( |
||||
(channel, externalJsonFile, playoutType) => new Playout |
||||
{ |
||||
ChannelId = channel.Id, |
||||
TemplateFile = externalJsonFile, |
||||
ProgramSchedulePlayoutType = playoutType |
||||
}); |
||||
|
||||
private static Task<Validation<BaseError, Channel>> ValidateChannel( |
||||
TvContext dbContext, |
||||
CreateTemplatePlayout createTemplatePlayout) => |
||||
dbContext.Channels |
||||
.Include(c => c.Playouts) |
||||
.SelectOneAsync(c => c.Id, c => c.Id == createTemplatePlayout.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 Validation<BaseError, string> ValidateTemplateFile(CreateTemplatePlayout request) |
||||
{ |
||||
if (!_localFileSystem.FileExists(request.TemplateFile)) |
||||
{ |
||||
return BaseError.New("Template file does not exist!"); |
||||
} |
||||
|
||||
return request.TemplateFile; |
||||
} |
||||
|
||||
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType( |
||||
CreateTemplatePlayout createTemplatePlayout) => |
||||
Optional(createTemplatePlayout.ProgramSchedulePlayoutType) |
||||
.Filter(playoutType => playoutType == ProgramSchedulePlayoutType.Template) |
||||
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must be Template"); |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
using ErsatzTV.Core; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public record UpdateTemplatePlayout(int PlayoutId, string TemplateFile) |
||||
: IRequest<Either<BaseError, PlayoutNameViewModel>>; |
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
using System.Threading.Channels; |
||||
using ErsatzTV.Application.Channels; |
||||
using ErsatzTV.Core; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Infrastructure.Data; |
||||
using ErsatzTV.Infrastructure.Extensions; |
||||
using Microsoft.EntityFrameworkCore; |
||||
|
||||
namespace ErsatzTV.Application.Playouts; |
||||
|
||||
public class |
||||
UpdateTemplatePlayoutHandler : IRequestHandler<UpdateTemplatePlayout, |
||||
Either<BaseError, PlayoutNameViewModel>> |
||||
{ |
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory; |
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; |
||||
|
||||
public UpdateTemplatePlayoutHandler( |
||||
IDbContextFactory<TvContext> dbContextFactory, |
||||
ChannelWriter<IBackgroundServiceRequest> workerChannel) |
||||
{ |
||||
_dbContextFactory = dbContextFactory; |
||||
_workerChannel = workerChannel; |
||||
} |
||||
|
||||
public async Task<Either<BaseError, PlayoutNameViewModel>> Handle( |
||||
UpdateTemplatePlayout request, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); |
||||
Validation<BaseError, Playout> validation = await Validate(dbContext, request); |
||||
return await validation.Apply(playout => ApplyUpdateRequest(dbContext, request, playout)); |
||||
} |
||||
|
||||
private async Task<PlayoutNameViewModel> ApplyUpdateRequest( |
||||
TvContext dbContext, |
||||
UpdateTemplatePlayout request, |
||||
Playout playout) |
||||
{ |
||||
playout.TemplateFile = request.TemplateFile; |
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0) |
||||
{ |
||||
await _workerChannel.WriteAsync(new RefreshChannelData(playout.Channel.Number)); |
||||
} |
||||
|
||||
return new PlayoutNameViewModel( |
||||
playout.Id, |
||||
playout.ProgramSchedulePlayoutType, |
||||
playout.Channel.Name, |
||||
playout.Channel.Number, |
||||
playout.Channel.ProgressMode, |
||||
playout.ProgramSchedule?.Name ?? string.Empty, |
||||
playout.TemplateFile, |
||||
playout.ExternalJsonFile, |
||||
playout.DailyRebuildTime); |
||||
} |
||||
|
||||
private static Task<Validation<BaseError, Playout>> Validate( |
||||
TvContext dbContext, |
||||
UpdateTemplatePlayout request) => |
||||
PlayoutMustExist(dbContext, request); |
||||
|
||||
private static Task<Validation<BaseError, Playout>> PlayoutMustExist( |
||||
TvContext dbContext, |
||||
UpdateTemplatePlayout updatePlayout) => |
||||
dbContext.Playouts |
||||
.Include(p => p.Channel) |
||||
.SelectOneAsync(p => p.Id, p => p.Id == updatePlayout.PlayoutId) |
||||
.Map(o => o.ToValidation<BaseError>("Playout does not exist.")); |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
public interface ITemplatePlayoutBuilder |
||||
{ |
||||
Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken); |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplate |
||||
{ |
||||
public List<PlayoutTemplateContentSearchItem> Content { get; set; } = []; |
||||
public List<PlayoutTemplateItem> Playout { get; set; } = []; |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateContent |
||||
{ |
||||
|
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateContentItem |
||||
{ |
||||
public string Key { get; set; } |
||||
public string Order { get; set; } |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateContentSearchItem : PlayoutTemplateContentItem |
||||
{ |
||||
public string Search { get; set; } |
||||
public string Query { get; set; } |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateCountItem : PlayoutTemplateItem |
||||
{ |
||||
public int Count { get; set; } |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateItem |
||||
{ |
||||
public string Content { get; set; } |
||||
} |
@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
|
||||
using YamlDotNet.Serialization; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplatePadToNextItem : PlayoutTemplateItem |
||||
{ |
||||
[YamlMember(Alias = "pad_to_next", ApplyNamingConventions = false)] |
||||
public int PadToNext { get; set; } |
||||
|
||||
public bool Trim { get; set; } |
||||
} |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplatePlayout |
||||
{ |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateRepeatItem : PlayoutTemplateItem |
||||
{ |
||||
public bool Repeat { get; set; } |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Extensions; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public abstract class PlayoutTemplateScheduler |
||||
{ |
||||
protected static TimeSpan DurationForMediaItem(MediaItem mediaItem) |
||||
{ |
||||
if (mediaItem is Image image) |
||||
{ |
||||
return TimeSpan.FromSeconds(image.ImageMetadata.Head().DurationSeconds ?? Image.DefaultSeconds); |
||||
} |
||||
|
||||
MediaVersion version = mediaItem.GetHeadVersion(); |
||||
return version.Duration; |
||||
} |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateSchedulerCount : PlayoutTemplateScheduler |
||||
{ |
||||
public static DateTimeOffset Schedule( |
||||
Playout playout, |
||||
DateTimeOffset currentTime, |
||||
PlayoutTemplateCountItem count, |
||||
IMediaCollectionEnumerator enumerator) |
||||
{ |
||||
for (int i = 0; i < count.Count; i++) |
||||
{ |
||||
foreach (MediaItem mediaItem in enumerator.Current) |
||||
{ |
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem); |
||||
|
||||
// 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, //blockItem.IncludeInProgramGuide ? FillerKind.None : FillerKind.GuideMode,
|
||||
//CustomTitle = scheduleItem.CustomTitle,
|
||||
//WatermarkId = scheduleItem.WatermarkId,
|
||||
//PreferredAudioLanguageCode = scheduleItem.PreferredAudioLanguageCode,
|
||||
//PreferredAudioTitle = scheduleItem.PreferredAudioTitle,
|
||||
//PreferredSubtitleLanguageCode = scheduleItem.PreferredSubtitleLanguageCode,
|
||||
//SubtitleMode = scheduleItem.SubtitleMode
|
||||
//GuideGroup = effectiveBlock.TemplateItemId,
|
||||
//GuideStart = effectiveBlock.Start.UtcDateTime,
|
||||
//GuideFinish = blockFinish.UtcDateTime,
|
||||
//BlockKey = JsonConvert.SerializeObject(effectiveBlock.BlockKey),
|
||||
//CollectionKey = JsonConvert.SerializeObject(collectionKey, JsonSettings),
|
||||
//CollectionEtag = collectionEtags[collectionKey]
|
||||
}; |
||||
|
||||
playout.Items.Add(playoutItem); |
||||
|
||||
currentTime += itemDuration; |
||||
enumerator.MoveNext(); |
||||
} |
||||
} |
||||
|
||||
return currentTime; |
||||
} |
||||
} |
@ -0,0 +1,80 @@
@@ -0,0 +1,80 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class PlayoutTemplateSchedulerPadToNext : PlayoutTemplateScheduler |
||||
{ |
||||
public static DateTimeOffset Schedule( |
||||
Playout playout, |
||||
DateTimeOffset currentTime, |
||||
PlayoutTemplatePadToNextItem padToNext, |
||||
IMediaCollectionEnumerator enumerator) |
||||
{ |
||||
int currentMinute = currentTime.Minute; |
||||
|
||||
int targetMinute = (currentMinute + padToNext.PadToNext - 1) / padToNext.PadToNext * padToNext.PadToNext; |
||||
|
||||
DateTimeOffset almostTargetTime = |
||||
currentTime - TimeSpan.FromMinutes(currentMinute) + TimeSpan.FromMinutes(targetMinute); |
||||
|
||||
var targetTime = new DateTimeOffset( |
||||
almostTargetTime.Year, |
||||
almostTargetTime.Month, |
||||
almostTargetTime.Day, |
||||
almostTargetTime.Hour, |
||||
almostTargetTime.Minute, |
||||
0, |
||||
almostTargetTime.Offset); |
||||
|
||||
// ensure filler works for content less than one minute
|
||||
if (targetTime <= currentTime) |
||||
targetTime = targetTime.AddMinutes(padToNext.PadToNext); |
||||
|
||||
bool done = false; |
||||
TimeSpan remainingToFill = targetTime - currentTime; |
||||
while (!done && enumerator.Current.IsSome && remainingToFill > TimeSpan.Zero && |
||||
remainingToFill >= enumerator.MinimumDuration) |
||||
{ |
||||
foreach (MediaItem mediaItem in enumerator.Current) |
||||
{ |
||||
TimeSpan itemDuration = DurationForMediaItem(mediaItem); |
||||
|
||||
var playoutItem = new PlayoutItem |
||||
{ |
||||
MediaItemId = mediaItem.Id, |
||||
Start = currentTime.UtcDateTime, |
||||
Finish = currentTime.UtcDateTime + itemDuration, |
||||
InPoint = TimeSpan.Zero, |
||||
OutPoint = itemDuration, |
||||
//GuideGroup = playoutBuilderState.NextGuideGroup,
|
||||
//FillerKind = fillerKind,
|
||||
//DisableWatermarks = !allowWatermarks
|
||||
}; |
||||
|
||||
if (remainingToFill - itemDuration >= TimeSpan.Zero) |
||||
{ |
||||
remainingToFill -= itemDuration; |
||||
playout.Items.Add(playoutItem); |
||||
enumerator.MoveNext(); |
||||
} |
||||
else if (padToNext.Trim) |
||||
{ |
||||
// trim item to exactly fit
|
||||
remainingToFill = TimeSpan.Zero; |
||||
playoutItem.Finish = targetTime.UtcDateTime; |
||||
playoutItem.OutPoint = playoutItem.Finish - playoutItem.Start; |
||||
playout.Items.Add(playoutItem); |
||||
enumerator.MoveNext(); |
||||
} |
||||
else |
||||
{ |
||||
// item won't fit; we're done for now
|
||||
done = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return targetTime; |
||||
} |
||||
} |
@ -0,0 +1,161 @@
@@ -0,0 +1,161 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using LanguageExt.UnsafeValueAccess; |
||||
using Microsoft.Extensions.Logging; |
||||
using YamlDotNet.Serialization; |
||||
using YamlDotNet.Serialization.NamingConventions; |
||||
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling; |
||||
|
||||
public class TemplatePlayoutBuilder( |
||||
ILocalFileSystem localFileSystem, |
||||
IConfigElementRepository configElementRepository, |
||||
IMediaCollectionRepository mediaCollectionRepository, |
||||
ILogger<TemplatePlayoutBuilder> logger) |
||||
: ITemplatePlayoutBuilder |
||||
{ |
||||
public async Task<Playout> Build(Playout playout, PlayoutBuildMode mode, CancellationToken cancellationToken) |
||||
{ |
||||
if (!localFileSystem.FileExists(playout.TemplateFile)) |
||||
{ |
||||
logger.LogWarning("Playout template file {File} does not exist; aborting.", playout.TemplateFile); |
||||
return playout; |
||||
} |
||||
|
||||
PlayoutTemplate playoutTemplate = await LoadTemplate(playout, cancellationToken); |
||||
|
||||
DateTimeOffset start = DateTimeOffset.Now; |
||||
int daysToBuild = await GetDaysToBuild(); |
||||
DateTimeOffset finish = start.AddDays(daysToBuild); |
||||
|
||||
if (mode is not PlayoutBuildMode.Reset) |
||||
{ |
||||
logger.LogWarning("Template playouts can only be reset; ignoring build mode {Mode}", mode.ToString()); |
||||
return playout; |
||||
} |
||||
|
||||
// these are only for reset
|
||||
playout.Seed = new Random().Next(); |
||||
playout.Items.Clear(); |
||||
|
||||
DateTimeOffset currentTime = start; |
||||
|
||||
// load content and content enumerators on demand
|
||||
Dictionary<string, IMediaCollectionEnumerator> enumerators = new(); |
||||
|
||||
var index = 0; |
||||
while (currentTime < finish) |
||||
{ |
||||
if (index >= playoutTemplate.Playout.Count) |
||||
{ |
||||
logger.LogInformation("Reached the end of the playout template; stopping"); |
||||
break; |
||||
} |
||||
|
||||
PlayoutTemplateItem playoutItem = playoutTemplate.Playout[index]; |
||||
|
||||
// repeat resets index into template playout
|
||||
if (playoutItem is PlayoutTemplateRepeatItem) |
||||
{ |
||||
index = 0; |
||||
continue; |
||||
} |
||||
|
||||
if (!enumerators.TryGetValue(playoutItem.Content, out IMediaCollectionEnumerator enumerator)) |
||||
{ |
||||
Option<IMediaCollectionEnumerator> maybeEnumerator = |
||||
await GetEnumeratorForContent(playout, playoutItem.Content, playoutTemplate, cancellationToken); |
||||
|
||||
if (maybeEnumerator.IsNone) |
||||
{ |
||||
logger.LogWarning("Unable to locate content with key {Key}", playoutItem.Content); |
||||
continue; |
||||
} |
||||
|
||||
foreach (IMediaCollectionEnumerator e in maybeEnumerator) |
||||
{ |
||||
enumerator = maybeEnumerator.ValueUnsafe(); |
||||
enumerators.Add(playoutItem.Content, enumerator); |
||||
} |
||||
} |
||||
|
||||
switch (playoutItem) |
||||
{ |
||||
case PlayoutTemplateCountItem count: |
||||
currentTime = PlayoutTemplateSchedulerCount.Schedule(playout, currentTime, count, enumerator); |
||||
break; |
||||
case PlayoutTemplatePadToNextItem padToNext: |
||||
currentTime = PlayoutTemplateSchedulerPadToNext.Schedule( |
||||
playout, |
||||
currentTime, |
||||
padToNext, |
||||
enumerator); |
||||
break; |
||||
} |
||||
|
||||
index++; |
||||
} |
||||
|
||||
return playout; |
||||
} |
||||
|
||||
private async Task<int> GetDaysToBuild() => |
||||
await configElementRepository |
||||
.GetValue<int>(ConfigElementKey.PlayoutDaysToBuild) |
||||
.IfNoneAsync(2); |
||||
|
||||
private async Task<Option<IMediaCollectionEnumerator>> GetEnumeratorForContent( |
||||
Playout playout, |
||||
string contentKey, |
||||
PlayoutTemplate playoutTemplate, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
int index = playoutTemplate.Content.FindIndex(c => c.Key == contentKey); |
||||
if (index < 0) |
||||
{ |
||||
return Option<IMediaCollectionEnumerator>.None; |
||||
} |
||||
|
||||
PlayoutTemplateContentSearchItem content = playoutTemplate.Content[index]; |
||||
List<MediaItem> items = await mediaCollectionRepository.GetSmartCollectionItems(content.Query); |
||||
var state = new CollectionEnumeratorState { Seed = playout.Seed + index, Index = 0 }; |
||||
switch (Enum.Parse<PlaybackOrder>(content.Order, true)) |
||||
{ |
||||
case PlaybackOrder.Chronological: |
||||
return new ChronologicalMediaCollectionEnumerator(items, state); |
||||
case PlaybackOrder.Shuffle: |
||||
// TODO: fix this
|
||||
var groupedMediaItems = items.Map(mi => new GroupedMediaItem(mi, null)).ToList(); |
||||
return new ShuffledMediaCollectionEnumerator(groupedMediaItems, state, cancellationToken); |
||||
} |
||||
|
||||
return Option<IMediaCollectionEnumerator>.None; |
||||
} |
||||
|
||||
private static async Task<PlayoutTemplate> LoadTemplate(Playout playout, CancellationToken cancellationToken) |
||||
{ |
||||
string yaml = await File.ReadAllTextAsync(playout.TemplateFile, cancellationToken); |
||||
|
||||
IDeserializer deserializer = new DeserializerBuilder() |
||||
.WithNamingConvention(CamelCaseNamingConvention.Instance) |
||||
.WithTypeDiscriminatingNodeDeserializer( |
||||
o => |
||||
{ |
||||
var keyMappings = new Dictionary<string, Type> |
||||
{ |
||||
{ "count", typeof(PlayoutTemplateCountItem) }, |
||||
{ "pad_to_next", typeof(PlayoutTemplatePadToNextItem) }, |
||||
{ "repeat", typeof(PlayoutTemplateRepeatItem) } |
||||
}; |
||||
|
||||
o.AddUniqueKeyTypeDiscriminator<PlayoutTemplateItem>(keyMappings); |
||||
}) |
||||
.Build(); |
||||
|
||||
return deserializer.Deserialize<PlayoutTemplate>(yaml); |
||||
} |
||||
|
||||
|
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations; |
||||
|
||||
#nullable disable |
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations |
||||
{ |
||||
/// <inheritdoc />
|
||||
public partial class Add_Playout_TemplateFile : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "TemplateFile", |
||||
table: "Playout", |
||||
type: "longtext", |
||||
nullable: true) |
||||
.Annotation("MySql:CharSet", "utf8mb4"); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "TemplateFile", |
||||
table: "Playout"); |
||||
} |
||||
} |
||||
} |
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_Playout_TemplateFile : Migration |
||||
{ |
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.AddColumn<string>( |
||||
name: "TemplateFile", |
||||
table: "Playout", |
||||
type: "TEXT", |
||||
nullable: true); |
||||
} |
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder) |
||||
{ |
||||
migrationBuilder.DropColumn( |
||||
name: "TemplateFile", |
||||
table: "Playout"); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
@implements IDisposable |
||||
|
||||
<MudDialog> |
||||
<DialogContent> |
||||
<MudContainer Class="mb-6"> |
||||
<MudText> |
||||
Edit the playout's template file |
||||
</MudText> |
||||
</MudContainer> |
||||
<MudTextField Label="Template File" @bind-Value="_templateFile"/> |
||||
</DialogContent> |
||||
<DialogActions> |
||||
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton> |
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(string.IsNullOrWhiteSpace(_templateFile))" OnClick="Submit"> |
||||
Save Changes |
||||
</MudButton> |
||||
</DialogActions> |
||||
</MudDialog> |
||||
|
||||
@code { |
||||
private readonly CancellationTokenSource _cts = new(); |
||||
|
||||
[Parameter] |
||||
public string TemplateFile { get; set; } |
||||
|
||||
[CascadingParameter] |
||||
MudDialogInstance MudDialog { get; set; } |
||||
|
||||
private string _templateFile; |
||||
|
||||
public void Dispose() |
||||
{ |
||||
_cts.Cancel(); |
||||
_cts.Dispose(); |
||||
} |
||||
|
||||
protected override void OnParametersSet() => _templateFile = TemplateFile; |
||||
|
||||
private void Submit() => MudDialog.Close(DialogResult.Ok(_templateFile)); |
||||
|
||||
private void Cancel() => MudDialog.Cancel(); |
||||
} |
Loading…
Reference in new issue