add experimental playout template system (#1808)
* add template playout kind * add template scheduler count * implement pad to next * only allow resetting template playouts * update changelog
This commit is contained in:
@@ -22,6 +22,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Default filler will stop scheduling when the next item would extend into primary content
|
||||
- Alternatively, default filler can be configured to `Trim To Fit`
|
||||
- In this case, the last item that would extend into primary content is trimmed to end exactly when the primary content starts
|
||||
- Add **experimental** playout type `Template`
|
||||
- This playout type uses a YAML file to declare content and describe how the playout should be built
|
||||
- Content currently supports search queries
|
||||
- Playout instructions currently include `count`, `pad to next`, and `repeat`
|
||||
- `count`: add the specified number of items (from the referenced content) to the playout
|
||||
- `pad to next`: add items from the referenced content until the wall clock is a multiple of the specified minutes value
|
||||
- `repeat`: continue building the playout from the first instruction in the template
|
||||
|
||||
### Fixed
|
||||
- Add basic cache busting to XMLTV image URLs
|
||||
|
||||
@@ -19,6 +19,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
{
|
||||
private readonly IBlockPlayoutBuilder _blockPlayoutBuilder;
|
||||
private readonly IBlockPlayoutFillerBuilder _blockPlayoutFillerBuilder;
|
||||
private readonly ITemplatePlayoutBuilder _templatePlayoutBuilder;
|
||||
private readonly IClient _client;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
@@ -33,6 +34,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
IPlayoutBuilder playoutBuilder,
|
||||
IBlockPlayoutBuilder blockPlayoutBuilder,
|
||||
IBlockPlayoutFillerBuilder blockPlayoutFillerBuilder,
|
||||
ITemplatePlayoutBuilder templatePlayoutBuilder,
|
||||
IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder,
|
||||
IFFmpegSegmenterService ffmpegSegmenterService,
|
||||
IEntityLocker entityLocker,
|
||||
@@ -43,6 +45,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
_playoutBuilder = playoutBuilder;
|
||||
_blockPlayoutBuilder = blockPlayoutBuilder;
|
||||
_blockPlayoutFillerBuilder = blockPlayoutFillerBuilder;
|
||||
_templatePlayoutBuilder = templatePlayoutBuilder;
|
||||
_externalJsonPlayoutBuilder = externalJsonPlayoutBuilder;
|
||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||
_entityLocker = entityLocker;
|
||||
@@ -74,6 +77,9 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
await _blockPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
|
||||
await _blockPlayoutFillerBuilder.Build(playout, request.Mode, cancellationToken);
|
||||
break;
|
||||
case ProgramSchedulePlayoutType.Template:
|
||||
await _templatePlayoutBuilder.Build(playout, request.Mode, cancellationToken);
|
||||
break;
|
||||
case ProgramSchedulePlayoutType.ExternalJson:
|
||||
await _externalJsonPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
|
||||
break;
|
||||
|
||||
@@ -12,5 +12,8 @@ public record CreateFloodPlayout(int ChannelId, int ProgramScheduleId)
|
||||
public record CreateBlockPlayout(int ChannelId)
|
||||
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Block);
|
||||
|
||||
public record CreateTemplatePlayout(int ChannelId, string TemplateFile)
|
||||
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Template);
|
||||
|
||||
public record CreateExternalJsonPlayout(int ChannelId, string ExternalJsonFile)
|
||||
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.ExternalJson);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -51,6 +51,7 @@ public class
|
||||
playout.Channel.Number,
|
||||
playout.Channel.ProgressMode,
|
||||
playout.ProgramSchedule?.Name ?? string.Empty,
|
||||
playout.TemplateFile,
|
||||
playout.ExternalJsonFile,
|
||||
playout.DailyRebuildTime);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
|
||||
playout.Channel.Number,
|
||||
playout.Channel.ProgressMode,
|
||||
playout.ProgramSchedule?.Name ?? string.Empty,
|
||||
playout.TemplateFile,
|
||||
playout.ExternalJsonFile,
|
||||
playout.DailyRebuildTime);
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
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."));
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public record PlayoutNameViewModel(
|
||||
string ChannelNumber,
|
||||
ChannelProgressMode ProgressMode,
|
||||
string ScheduleName,
|
||||
string TemplateFile,
|
||||
string ExternalJsonFile,
|
||||
TimeSpan? DbDailyRebuildTime)
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<Playou
|
||||
p.Channel.Number,
|
||||
p.Channel.ProgressMode,
|
||||
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
|
||||
p.TemplateFile,
|
||||
p.ExternalJsonFile,
|
||||
p.DailyRebuildTime))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -22,6 +22,7 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
|
||||
public Task<List<MediaItem>> GetItems(int id) => _data[id].ToList().AsTask();
|
||||
public Task<List<MediaItem>> GetMultiCollectionItems(int id) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask();
|
||||
public Task<List<MediaItem>> GetSmartCollectionItems(string query) => throw new NotSupportedException();
|
||||
public Task<List<MediaItem>> GetPlaylistItems(int id) => throw new NotSupportedException();
|
||||
public Task<List<Movie>> GetMovie(int id) => throw new NotSupportedException();
|
||||
public Task<List<Episode>> GetEpisode(int id) => throw new NotSupportedException();
|
||||
|
||||
@@ -10,6 +10,7 @@ public class Playout
|
||||
public int? ProgramScheduleId { get; set; }
|
||||
public ProgramSchedule ProgramSchedule { get; set; }
|
||||
public string ExternalJsonFile { get; set; }
|
||||
public string TemplateFile { get; set; }
|
||||
public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
|
||||
public ProgramSchedulePlayoutType ProgramSchedulePlayoutType { get; set; }
|
||||
public List<PlayoutItem> Items { get; set; }
|
||||
|
||||
@@ -5,6 +5,7 @@ public enum ProgramSchedulePlayoutType
|
||||
None = 0,
|
||||
Flood = 1,
|
||||
Block = 2,
|
||||
Template = 3,
|
||||
|
||||
ExternalJson = 20
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="16.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -11,6 +11,7 @@ public interface IMediaCollectionRepository
|
||||
Task<List<MediaItem>> GetItems(int id);
|
||||
Task<List<MediaItem>> GetMultiCollectionItems(int id);
|
||||
Task<List<MediaItem>> GetSmartCollectionItems(int id);
|
||||
Task<List<MediaItem>> GetSmartCollectionItems(string query);
|
||||
Task<List<MediaItem>> GetPlaylistItems(int id);
|
||||
Task<List<Movie>> GetMovie(int id);
|
||||
Task<List<Episode>> GetEpisode(int id);
|
||||
|
||||
@@ -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 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
|
||||
public class PlayoutTemplate
|
||||
{
|
||||
public List<PlayoutTemplateContentSearchItem> Content { get; set; } = [];
|
||||
public List<PlayoutTemplateItem> Playout { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
|
||||
public class PlayoutTemplateContent
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
|
||||
public class PlayoutTemplateContentSearchItem : PlayoutTemplateContentItem
|
||||
{
|
||||
public string Search { get; set; }
|
||||
public string Query { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
|
||||
public class PlayoutTemplateCountItem : PlayoutTemplateItem
|
||||
{
|
||||
public int Count { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
|
||||
public class PlayoutTemplateItem
|
||||
{
|
||||
public string Content { get; set; }
|
||||
}
|
||||
@@ -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 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
|
||||
public class PlayoutTemplatePlayout
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
|
||||
public class PlayoutTemplateRepeatItem : PlayoutTemplateItem
|
||||
{
|
||||
public bool Repeat { get; set; }
|
||||
}
|
||||
@@ -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 @@
|
||||
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 @@
|
||||
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 @@
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Generated
+5860
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1714,6 +1714,9 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<int>("Seed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TemplateFile")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
Generated
+5699
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1627,6 +1627,9 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<int>("Seed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TemplateFile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
@@ -346,15 +346,25 @@ public class MediaCollectionRepository : IMediaCollectionRepository
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var result = new List<MediaItem>();
|
||||
|
||||
Option<SmartCollection> maybeCollection = await dbContext.SmartCollections
|
||||
.SelectOneAsync(sc => sc.Id, sc => sc.Id == id);
|
||||
|
||||
foreach (SmartCollection collection in maybeCollection)
|
||||
{
|
||||
return await GetSmartCollectionItems(collection.Query);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public async Task<List<MediaItem>> GetSmartCollectionItems(string query)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var result = new List<MediaItem>();
|
||||
|
||||
// elasticsearch doesn't like when we ask for a limit of zero, so use 10,000
|
||||
SearchResult searchResults = await _searchIndex.Search(_client, collection.Query, 0, 10_000);
|
||||
SearchResult searchResults = await _searchIndex.Search(_client, query, 0, 10_000);
|
||||
|
||||
var movieIds = searchResults.Items
|
||||
.Filter(i => i.Type == LuceneSearchIndex.MovieType)
|
||||
@@ -408,7 +418,6 @@ public class MediaCollectionRepository : IMediaCollectionRepository
|
||||
.Map(i => i.Id)
|
||||
.ToList();
|
||||
result.AddRange(await GetImageItems(dbContext, imageIds));
|
||||
}
|
||||
|
||||
return result.DistinctBy(x => x.Id).ToList();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
case PlayoutKind.ExternalJson:
|
||||
<span>Add External Json Playout</span>
|
||||
break;
|
||||
case PlayoutKind.Template:
|
||||
<span>Add Template Playout</span>
|
||||
break;
|
||||
case PlayoutKind.Block:
|
||||
<span>Add Block Playout</span>
|
||||
break;
|
||||
@@ -39,12 +42,15 @@
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (Kind == PlayoutKind.ExternalJson)
|
||||
@switch (Kind)
|
||||
{
|
||||
case PlayoutKind.ExternalJson:
|
||||
<MudTextField Label="External Json File" @bind-Value="_model.ExternalJsonFile" For="@(() => _model.ExternalJsonFile)"/>
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(Kind))
|
||||
{
|
||||
break;
|
||||
case PlayoutKind.Template:
|
||||
<MudTextField Label="Template File" @bind-Value="_model.TemplateFile" For="@(() => _model.TemplateFile)"/>
|
||||
break;
|
||||
default:
|
||||
<MudSelect Class="mt-3"
|
||||
T="ProgramScheduleViewModel"
|
||||
Label="Schedule"
|
||||
@@ -54,6 +60,7 @@
|
||||
<MudSelectItem Value="@schedule">@schedule.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
break;
|
||||
}
|
||||
</MudCardContent>
|
||||
<MudCardActions>
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
Add Block Playout
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="This feature is experimental">
|
||||
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Template}")">
|
||||
Add Template Playout
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="This feature is experimental">
|
||||
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")">
|
||||
Add External Json Playout
|
||||
@@ -63,6 +68,9 @@
|
||||
case ProgramSchedulePlayoutType.Block:
|
||||
<span>Block</span>
|
||||
break;
|
||||
case ProgramSchedulePlayoutType.Template:
|
||||
<span>Template</span>
|
||||
break;
|
||||
case ProgramSchedulePlayoutType.ExternalJson:
|
||||
<span>External Json</span>
|
||||
break;
|
||||
@@ -123,6 +131,22 @@
|
||||
<div style="width: 48px"></div>
|
||||
<div style="width: 48px"></div>
|
||||
}
|
||||
else if (context.PlayoutType == ProgramSchedulePlayoutType.Template)
|
||||
{
|
||||
<MudTooltip Text="Edit Template File">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
OnClick="@(_ => EditTemplateFile(context))">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Reset Playout">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
OnClick="@(_ => ResetPlayout(context))">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
<div style="width: 48px"></div>
|
||||
}
|
||||
else if (context.PlayoutType == ProgramSchedulePlayoutType.Block)
|
||||
{
|
||||
<MudTooltip Text="Edit Playout">
|
||||
@@ -232,8 +256,8 @@
|
||||
|
||||
private async Task PlayoutSelected(PlayoutNameViewModel playout)
|
||||
{
|
||||
// only show details for flood and block playouts
|
||||
_selectedPlayoutId = playout.PlayoutType is ProgramSchedulePlayoutType.Flood or ProgramSchedulePlayoutType.Block
|
||||
// only show details for flood, block and template playouts
|
||||
_selectedPlayoutId = playout.PlayoutType is ProgramSchedulePlayoutType.Flood or ProgramSchedulePlayoutType.Block or ProgramSchedulePlayoutType.Template
|
||||
? playout.PlayoutId
|
||||
: null;
|
||||
|
||||
@@ -262,6 +286,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EditTemplateFile(PlayoutNameViewModel playout)
|
||||
{
|
||||
var parameters = new DialogParameters { { "TemplateFile", $"{playout.TemplateFile}" } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge };
|
||||
|
||||
IDialogReference dialog = await Dialog.ShowAsync<EditTemplateFileDialog>("Edit Template File", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (!result.Canceled)
|
||||
{
|
||||
await Mediator.Send(new UpdateTemplatePlayout(playout.PlayoutId, result.Data as string ?? playout.TemplateFile), _cts.Token);
|
||||
if (_table != null)
|
||||
{
|
||||
await _table.ReloadServerData();
|
||||
}
|
||||
|
||||
_selectedPlayoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeletePlayout(PlayoutNameViewModel playout)
|
||||
{
|
||||
var parameters = new DialogParameters { { "EntityType", "playout" }, { "EntityName", $"{playout.ScheduleName} on {playout.ChannelNumber} - {playout.ChannelName}" } };
|
||||
|
||||
@@ -3,5 +3,6 @@ namespace ErsatzTV;
|
||||
public static class PlayoutKind
|
||||
{
|
||||
public const string ExternalJson = "externaljson";
|
||||
public const string Template = "template";
|
||||
public const string Block = "block";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -36,6 +36,7 @@ using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling.BlockScheduling;
|
||||
using ErsatzTV.Core.Scheduling.TemplateScheduling;
|
||||
using ErsatzTV.Core.Trakt;
|
||||
using ErsatzTV.FFmpeg.Capabilities;
|
||||
using ErsatzTV.FFmpeg.Pipeline;
|
||||
@@ -655,6 +656,7 @@ public class Startup
|
||||
services.AddScoped<IBlockPlayoutBuilder, BlockPlayoutBuilder>();
|
||||
services.AddScoped<IBlockPlayoutPreviewBuilder, BlockPlayoutPreviewBuilder>();
|
||||
services.AddScoped<IBlockPlayoutFillerBuilder, BlockPlayoutFillerBuilder>();
|
||||
services.AddScoped<ITemplatePlayoutBuilder, TemplatePlayoutBuilder>();
|
||||
services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>();
|
||||
services.AddScoped<IPlayoutTimeShifter, PlayoutTimeShifter>();
|
||||
services.AddScoped<IImageCache, ImageCache>();
|
||||
|
||||
@@ -10,11 +10,13 @@ public class PlayoutEditViewModel
|
||||
public ChannelViewModel Channel { get; set; }
|
||||
public ProgramScheduleViewModel ProgramSchedule { get; set; }
|
||||
public string ExternalJsonFile { get; set; }
|
||||
public string TemplateFile { get; set; }
|
||||
|
||||
public CreatePlayout ToCreate() =>
|
||||
Kind switch
|
||||
{
|
||||
PlayoutKind.ExternalJson => new CreateExternalJsonPlayout(Channel.Id, ExternalJsonFile),
|
||||
PlayoutKind.Template => new CreateTemplatePlayout(Channel.Id, TemplateFile),
|
||||
PlayoutKind.Block => new CreateBlockPlayout(Channel.Id),
|
||||
_ => new CreateFloodPlayout(Channel.Id, ProgramSchedule.Id)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user