add xmltv block behavior setting (#2336)
* replace playout externaljsonfile and templatefile with schedulefile * add scripted schedule-based playout * wip - not functional yet * temp disable scripted playout creation * allow fast-forwarding block playouts * add xmltv block behavior setting
This commit is contained in:
@@ -57,6 +57,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Supports quick and deep scans
|
||||
- Can be triggered from the `Scan` button on show pages
|
||||
- Can be triggered by API call to `/api/libraries/{library-id}/scan-show`
|
||||
- Add XMLTV setting `XMLTV Block Behavior` to control how block schedules appear in the EPG
|
||||
- `Split Time Evenly` - default (existing) behavior; block time is split among all items that are visible in the EPG
|
||||
- `Use Actual Times` - actual times are used for all items that are visible in the EPG
|
||||
- This will introduce EPG gaps when filler is used, or when items are hidden from the EPG
|
||||
|
||||
### Fix
|
||||
- Fix database operations that were slowing down playout builds
|
||||
|
||||
@@ -194,6 +194,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
{
|
||||
case PlayoutScheduleKind.Classic:
|
||||
case PlayoutScheduleKind.Sequential:
|
||||
case PlayoutScheduleKind.Scripted:
|
||||
var floodSorted = playouts
|
||||
.Collect(p => p.Items)
|
||||
.OrderBy(pi => pi.Start)
|
||||
@@ -230,7 +231,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
xml);
|
||||
break;
|
||||
case PlayoutScheduleKind.ExternalJson:
|
||||
var externalJsonSorted = (await CollectExternalJsonItems(playout.ExternalJsonFile))
|
||||
var externalJsonSorted = (await CollectExternalJsonItems(playout.ScheduleFile))
|
||||
.Filter(pi => pi.StartOffset <= finish)
|
||||
.ToList();
|
||||
|
||||
@@ -362,53 +363,99 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
|
||||
.GetValue<XmltvTimeZone>(ConfigElementKey.XmltvTimeZone)
|
||||
.IfNoneAsync(XmltvTimeZone.Local);
|
||||
|
||||
XmltvBlockBehavior xmltvBlockBehavior = await _configElementRepository
|
||||
.GetValue<XmltvBlockBehavior>(ConfigElementKey.XmltvBlockBehavior)
|
||||
.IfNoneAsync(XmltvBlockBehavior.SplitTimeEvenly);
|
||||
|
||||
var groups = sorted.GroupBy(s => new { s.GuideStart, s.GuideFinish, s.GuideGroup });
|
||||
foreach (var group in groups)
|
||||
{
|
||||
DateTime groupStart = group.Key.GuideStart!.Value;
|
||||
DateTime groupFinish = group.Key.GuideFinish!.Value;
|
||||
TimeSpan groupDuration = groupFinish - groupStart;
|
||||
|
||||
var itemsToInclude = group.Filter(g => g.FillerKind is FillerKind.None).ToList();
|
||||
if (itemsToInclude.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
TimeSpan perItem = groupDuration / itemsToInclude.Count;
|
||||
|
||||
DateTimeOffset currentStart = xmltvTimeZone switch
|
||||
switch (xmltvBlockBehavior)
|
||||
{
|
||||
XmltvTimeZone.Utc => new DateTimeOffset(groupStart, TimeSpan.Zero),
|
||||
_ => new DateTimeOffset(groupStart, TimeSpan.Zero).ToLocalTime()
|
||||
};
|
||||
case XmltvBlockBehavior.UseActualTimes:
|
||||
foreach (PlayoutItem item in itemsToInclude)
|
||||
{
|
||||
DateTimeOffset actualStart = xmltvTimeZone switch
|
||||
{
|
||||
XmltvTimeZone.Utc => new DateTimeOffset(item.Start, TimeSpan.Zero),
|
||||
_ => new DateTimeOffset(item.Start, TimeSpan.Zero).ToLocalTime()
|
||||
};
|
||||
|
||||
DateTimeOffset currentFinish = currentStart + perItem;
|
||||
DateTimeOffset actualFinish = xmltvTimeZone switch
|
||||
{
|
||||
XmltvTimeZone.Utc => new DateTimeOffset(item.Finish, TimeSpan.Zero),
|
||||
_ => new DateTimeOffset(item.Finish, TimeSpan.Zero).ToLocalTime()
|
||||
};
|
||||
|
||||
foreach (PlayoutItem item in itemsToInclude)
|
||||
{
|
||||
string start = currentStart.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
|
||||
.Replace(":", string.Empty);
|
||||
string stop = currentFinish.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
|
||||
.Replace(":", string.Empty);
|
||||
string start = actualStart.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
|
||||
.Replace(":", string.Empty);
|
||||
string stop = actualFinish.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
|
||||
.Replace(":", string.Empty);
|
||||
|
||||
await WriteItemToXml(
|
||||
request,
|
||||
item,
|
||||
start,
|
||||
stop,
|
||||
false,
|
||||
templateContext,
|
||||
movieTemplate,
|
||||
episodeTemplate,
|
||||
musicVideoTemplate,
|
||||
songTemplate,
|
||||
otherVideoTemplate,
|
||||
minifier,
|
||||
xml);
|
||||
await WriteItemToXml(
|
||||
request,
|
||||
item,
|
||||
start,
|
||||
stop,
|
||||
false,
|
||||
templateContext,
|
||||
movieTemplate,
|
||||
episodeTemplate,
|
||||
musicVideoTemplate,
|
||||
songTemplate,
|
||||
otherVideoTemplate,
|
||||
minifier,
|
||||
xml);
|
||||
}
|
||||
break;
|
||||
case XmltvBlockBehavior.SplitTimeEvenly:
|
||||
default:
|
||||
DateTime groupStart = group.Key.GuideStart!.Value;
|
||||
DateTime groupFinish = group.Key.GuideFinish!.Value;
|
||||
TimeSpan groupDuration = groupFinish - groupStart;
|
||||
|
||||
currentStart = currentFinish;
|
||||
currentFinish += perItem;
|
||||
TimeSpan perItem = groupDuration / itemsToInclude.Count;
|
||||
|
||||
DateTimeOffset currentStart = xmltvTimeZone switch
|
||||
{
|
||||
XmltvTimeZone.Utc => new DateTimeOffset(groupStart, TimeSpan.Zero),
|
||||
_ => new DateTimeOffset(groupStart, TimeSpan.Zero).ToLocalTime()
|
||||
};
|
||||
|
||||
DateTimeOffset currentFinish = currentStart + perItem;
|
||||
|
||||
foreach (PlayoutItem item in itemsToInclude)
|
||||
{
|
||||
string start = currentStart.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
|
||||
.Replace(":", string.Empty);
|
||||
string stop = currentFinish.ToString("yyyyMMddHHmmss zzz", CultureInfo.InvariantCulture)
|
||||
.Replace(":", string.Empty);
|
||||
|
||||
await WriteItemToXml(
|
||||
request,
|
||||
item,
|
||||
start,
|
||||
stop,
|
||||
false,
|
||||
templateContext,
|
||||
movieTemplate,
|
||||
episodeTemplate,
|
||||
musicVideoTemplate,
|
||||
songTemplate,
|
||||
otherVideoTemplate,
|
||||
minifier,
|
||||
xml);
|
||||
|
||||
currentStart = currentFinish;
|
||||
currentFinish += perItem;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public class UpdateXmltvSettingsHandler(
|
||||
{
|
||||
await configElementRepository.Upsert(ConfigElementKey.XmltvTimeZone, xmltvSettings.TimeZone);
|
||||
await configElementRepository.Upsert(ConfigElementKey.XmltvDaysToBuild, xmltvSettings.DaysToBuild);
|
||||
await configElementRepository.Upsert(ConfigElementKey.XmltvBlockBehavior, xmltvSettings.BlockBehavior);
|
||||
|
||||
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
|
||||
@@ -13,10 +13,14 @@ public class GetXmltvSettingsHandler(IConfigElementRepository configElementRepos
|
||||
Option<XmltvTimeZone> maybeTimeZone =
|
||||
await configElementRepository.GetValue<XmltvTimeZone>(ConfigElementKey.XmltvTimeZone);
|
||||
|
||||
Option<XmltvBlockBehavior> maybeBlockBehavior =
|
||||
await configElementRepository.GetValue<XmltvBlockBehavior>(ConfigElementKey.XmltvBlockBehavior);
|
||||
|
||||
return new XmltvSettingsViewModel
|
||||
{
|
||||
DaysToBuild = await daysToBuild.IfNoneAsync(2),
|
||||
TimeZone = await maybeTimeZone.IfNoneAsync(XmltvTimeZone.Local)
|
||||
TimeZone = await maybeTimeZone.IfNoneAsync(XmltvTimeZone.Local),
|
||||
BlockBehavior = await maybeBlockBehavior.IfNoneAsync(XmltvBlockBehavior.SplitTimeEvenly)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Application.Configuration;
|
||||
|
||||
public enum XmltvBlockBehavior
|
||||
{
|
||||
SplitTimeEvenly = 0,
|
||||
UseActualTimes = 1
|
||||
}
|
||||
@@ -4,4 +4,5 @@ public class XmltvSettingsViewModel
|
||||
{
|
||||
public int DaysToBuild { get; set; }
|
||||
public XmltvTimeZone TimeZone { get; set; }
|
||||
public XmltvBlockBehavior BlockBehavior { get; set; }
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
private readonly IPlayoutBuilder _playoutBuilder;
|
||||
private readonly IPlayoutTimeShifter _playoutTimeShifter;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
|
||||
private readonly IYamlPlayoutBuilder _yamlPlayoutBuilder;
|
||||
private readonly ISequentialPlayoutBuilder _sequentialPlayoutBuilder;
|
||||
private readonly IScriptedPlayoutBuilder _scriptedPlayoutBuilder;
|
||||
|
||||
public BuildPlayoutHandler(
|
||||
IClient client,
|
||||
@@ -37,7 +38,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
IPlayoutBuilder playoutBuilder,
|
||||
IBlockPlayoutBuilder blockPlayoutBuilder,
|
||||
IBlockPlayoutFillerBuilder blockPlayoutFillerBuilder,
|
||||
IYamlPlayoutBuilder yamlPlayoutBuilder,
|
||||
ISequentialPlayoutBuilder sequentialPlayoutBuilder,
|
||||
IScriptedPlayoutBuilder scriptedPlayoutBuilder,
|
||||
IExternalJsonPlayoutBuilder externalJsonPlayoutBuilder,
|
||||
IFFmpegSegmenterService ffmpegSegmenterService,
|
||||
IEntityLocker entityLocker,
|
||||
@@ -49,7 +51,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
_playoutBuilder = playoutBuilder;
|
||||
_blockPlayoutBuilder = blockPlayoutBuilder;
|
||||
_blockPlayoutFillerBuilder = blockPlayoutFillerBuilder;
|
||||
_yamlPlayoutBuilder = yamlPlayoutBuilder;
|
||||
_sequentialPlayoutBuilder = sequentialPlayoutBuilder;
|
||||
_scriptedPlayoutBuilder = scriptedPlayoutBuilder;
|
||||
_externalJsonPlayoutBuilder = externalJsonPlayoutBuilder;
|
||||
_ffmpegSegmenterService = ffmpegSegmenterService;
|
||||
_entityLocker = entityLocker;
|
||||
@@ -116,7 +119,12 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
switch (playout.ScheduleKind)
|
||||
{
|
||||
case PlayoutScheduleKind.Block:
|
||||
result = await _blockPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
|
||||
result = await _blockPlayoutBuilder.Build(
|
||||
request.Start,
|
||||
playout,
|
||||
referenceData,
|
||||
request.Mode,
|
||||
cancellationToken);
|
||||
result = await _blockPlayoutFillerBuilder.Build(
|
||||
playout,
|
||||
referenceData,
|
||||
@@ -125,7 +133,15 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
|
||||
cancellationToken);
|
||||
break;
|
||||
case PlayoutScheduleKind.Sequential:
|
||||
result = await _yamlPlayoutBuilder.Build(
|
||||
result = await _sequentialPlayoutBuilder.Build(
|
||||
request.Start,
|
||||
playout,
|
||||
referenceData,
|
||||
request.Mode,
|
||||
cancellationToken);
|
||||
break;
|
||||
case PlayoutScheduleKind.Scripted:
|
||||
result = await _scriptedPlayoutBuilder.Build(
|
||||
request.Start,
|
||||
playout,
|
||||
referenceData,
|
||||
|
||||
@@ -53,7 +53,7 @@ public class CreateExternalJsonPlayoutHandler
|
||||
.Apply((channel, externalJsonFile, scheduleKind) => new Playout
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
ExternalJsonFile = externalJsonFile,
|
||||
ScheduleFile = externalJsonFile,
|
||||
ScheduleKind = scheduleKind
|
||||
});
|
||||
|
||||
@@ -74,12 +74,12 @@ public class CreateExternalJsonPlayoutHandler
|
||||
|
||||
private Validation<BaseError, string> ValidateExternalJsonFile(CreateExternalJsonPlayout request)
|
||||
{
|
||||
if (!_localFileSystem.FileExists(request.ExternalJsonFile))
|
||||
if (!_localFileSystem.FileExists(request.ScheduleFile))
|
||||
{
|
||||
return BaseError.New("External Json File does not exist!");
|
||||
}
|
||||
|
||||
return request.ExternalJsonFile;
|
||||
return request.ScheduleFile;
|
||||
}
|
||||
|
||||
private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind(
|
||||
|
||||
@@ -12,8 +12,11 @@ public record CreateClassicPlayout(int ChannelId, int ProgramScheduleId)
|
||||
public record CreateBlockPlayout(int ChannelId)
|
||||
: CreatePlayout(ChannelId, PlayoutScheduleKind.Block);
|
||||
|
||||
public record CreateSequentialPlayout(int ChannelId, string TemplateFile)
|
||||
public record CreateSequentialPlayout(int ChannelId, string ScheduleFile)
|
||||
: CreatePlayout(ChannelId, PlayoutScheduleKind.Sequential);
|
||||
|
||||
public record CreateExternalJsonPlayout(int ChannelId, string ExternalJsonFile)
|
||||
public record CreateScriptedPlayout(int ChannelId, string ScheduleFile)
|
||||
: CreatePlayout(ChannelId, PlayoutScheduleKind.Scripted);
|
||||
|
||||
public record CreateExternalJsonPlayout(int ChannelId, string ScheduleFile)
|
||||
: CreatePlayout(ChannelId, PlayoutScheduleKind.ExternalJson);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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 CreateScriptedPlayoutHandler
|
||||
: IRequestHandler<CreateScriptedPlayout, Either<BaseError, CreatePlayoutResponse>>
|
||||
{
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
|
||||
public CreateScriptedPlayoutHandler(
|
||||
ILocalFileSystem localFileSystem,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel,
|
||||
IDbContextFactory<TvContext> dbContextFactory)
|
||||
{
|
||||
_localFileSystem = localFileSystem;
|
||||
_channel = channel;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
|
||||
CreateScriptedPlayout 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));
|
||||
if (playout.Channel.PlayoutMode is ChannelPlayoutMode.OnDemand)
|
||||
{
|
||||
await _channel.WriteAsync(new TimeShiftOnDemandPlayout(playout.Id, DateTimeOffset.Now, false));
|
||||
}
|
||||
|
||||
await _channel.WriteAsync(new RefreshChannelList());
|
||||
return new CreatePlayoutResponse(playout.Id);
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate(
|
||||
TvContext dbContext,
|
||||
CreateScriptedPlayout request) =>
|
||||
(await ValidateChannel(dbContext, request), ValidateScheduleFile(request), ValidateScheduleKind(request))
|
||||
.Apply((channel, yamlFile, scheduleKind) => new Playout
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
ScheduleFile = yamlFile,
|
||||
ScheduleKind = scheduleKind,
|
||||
Seed = new Random().Next()
|
||||
});
|
||||
|
||||
private static Task<Validation<BaseError, Channel>> ValidateChannel(
|
||||
TvContext dbContext,
|
||||
CreateScriptedPlayout createScriptedPlayout) =>
|
||||
dbContext.Channels
|
||||
.Include(c => c.Playouts)
|
||||
.SelectOneAsync(c => c.Id, c => c.Id == createScriptedPlayout.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> ValidateScheduleFile(CreateScriptedPlayout request)
|
||||
{
|
||||
if (!_localFileSystem.FileExists(request.ScheduleFile))
|
||||
{
|
||||
return BaseError.New("Scripted schedule does not exist!");
|
||||
}
|
||||
|
||||
return request.ScheduleFile;
|
||||
}
|
||||
|
||||
private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind(
|
||||
CreateScriptedPlayout createScriptedPlayout) =>
|
||||
Optional(createScriptedPlayout.ScheduleKind)
|
||||
.Filter(scheduleKind => scheduleKind == PlayoutScheduleKind.Scripted)
|
||||
.ToValidation<BaseError>("[ScheduleKind] must be Scripted");
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class CreateSequentialPlayoutHandler
|
||||
.Apply((channel, yamlFile, scheduleKind) => new Playout
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
TemplateFile = yamlFile,
|
||||
ScheduleFile = yamlFile,
|
||||
ScheduleKind = scheduleKind,
|
||||
Seed = new Random().Next()
|
||||
});
|
||||
@@ -80,17 +80,17 @@ public class CreateSequentialPlayoutHandler
|
||||
|
||||
private Validation<BaseError, string> ValidateYamlFile(CreateSequentialPlayout request)
|
||||
{
|
||||
if (!_localFileSystem.FileExists(request.TemplateFile))
|
||||
if (!_localFileSystem.FileExists(request.ScheduleFile))
|
||||
{
|
||||
return BaseError.New("YAML file does not exist!");
|
||||
return BaseError.New("Sequential schedule does not exist!");
|
||||
}
|
||||
|
||||
return request.TemplateFile;
|
||||
return request.ScheduleFile;
|
||||
}
|
||||
|
||||
private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind(
|
||||
CreateSequentialPlayout createSequentialPlayout) =>
|
||||
Optional(createSequentialPlayout.ScheduleKind)
|
||||
.Filter(scheduleKind => scheduleKind == PlayoutScheduleKind.Sequential)
|
||||
.ToValidation<BaseError>("[ScheduleKind] must be YAML");
|
||||
.ToValidation<BaseError>("[ScheduleKind] must be Sequential");
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ public class ResetAllPlayoutsHandler(
|
||||
case PlayoutScheduleKind.Classic:
|
||||
case PlayoutScheduleKind.Block:
|
||||
case PlayoutScheduleKind.Sequential:
|
||||
case PlayoutScheduleKind.Scripted:
|
||||
if (!locker.IsPlayoutLocked(playout.Id))
|
||||
{
|
||||
await channel.WriteAsync(
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
namespace ErsatzTV.Application.Playouts;
|
||||
|
||||
public record UpdateExternalJsonPlayout(int PlayoutId, string ExternalJsonFile)
|
||||
public record UpdateExternalJsonPlayout(int PlayoutId, string ScheduleFile)
|
||||
: IRequest<Either<BaseError, PlayoutNameViewModel>>;
|
||||
|
||||
@@ -37,7 +37,7 @@ public class
|
||||
UpdateExternalJsonPlayout request,
|
||||
Playout playout)
|
||||
{
|
||||
playout.ExternalJsonFile = request.ExternalJsonFile;
|
||||
playout.ScheduleFile = request.ScheduleFile;
|
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
@@ -51,8 +51,7 @@ public class
|
||||
playout.Channel.Number,
|
||||
playout.Channel.PlayoutMode,
|
||||
playout.ProgramSchedule?.Name ?? string.Empty,
|
||||
playout.TemplateFile,
|
||||
playout.ExternalJsonFile,
|
||||
playout.ScheduleFile,
|
||||
playout.DailyRebuildTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,7 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
|
||||
playout.Channel.Number,
|
||||
playout.Channel.PlayoutMode,
|
||||
playout.ProgramSchedule?.Name ?? string.Empty,
|
||||
playout.TemplateFile,
|
||||
playout.ExternalJsonFile,
|
||||
playout.ScheduleFile,
|
||||
playout.DailyRebuildTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
namespace ErsatzTV.Application.Playouts;
|
||||
|
||||
public record UpdateSequentialPlayout(int PlayoutId, string TemplateFile)
|
||||
public record UpdateSequentialPlayout(int PlayoutId, string ScheduleFile)
|
||||
: IRequest<Either<BaseError, PlayoutNameViewModel>>;
|
||||
|
||||
@@ -37,7 +37,7 @@ public class
|
||||
UpdateSequentialPlayout request,
|
||||
Playout playout)
|
||||
{
|
||||
playout.TemplateFile = request.TemplateFile;
|
||||
playout.ScheduleFile = request.ScheduleFile;
|
||||
|
||||
if (await dbContext.SaveChangesAsync() > 0)
|
||||
{
|
||||
@@ -51,8 +51,7 @@ public class
|
||||
playout.Channel.Number,
|
||||
playout.Channel.PlayoutMode,
|
||||
playout.ProgramSchedule?.Name ?? string.Empty,
|
||||
playout.TemplateFile,
|
||||
playout.ExternalJsonFile,
|
||||
playout.ScheduleFile,
|
||||
playout.DailyRebuildTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,10 @@ public record PlayoutNameViewModel(
|
||||
string ChannelNumber,
|
||||
ChannelPlayoutMode PlayoutMode,
|
||||
string ScheduleName,
|
||||
string TemplateFile,
|
||||
string ExternalJsonFile,
|
||||
string ScheduleFile,
|
||||
TimeSpan? DbDailyRebuildTime)
|
||||
{
|
||||
public Option<TimeSpan> DailyRebuildTime => Optional(DbDailyRebuildTime);
|
||||
|
||||
public string TemplateFile { get; set; } = TemplateFile;
|
||||
public string ScheduleFile { get; set; } = ScheduleFile;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<Playou
|
||||
p.Channel.Number,
|
||||
p.Channel.PlayoutMode,
|
||||
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
|
||||
p.TemplateFile,
|
||||
p.ExternalJsonFile,
|
||||
p.ScheduleFile,
|
||||
p.DailyRebuildTime))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -24,8 +24,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory
|
||||
p.Channel.Number,
|
||||
p.Channel.PlayoutMode,
|
||||
p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name,
|
||||
p.TemplateFile,
|
||||
p.ExternalJsonFile,
|
||||
p.ScheduleFile,
|
||||
p.DailyRebuildTime));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa
|
||||
|
||||
Option<Playout> maybePlayout = await dbContext.Playouts
|
||||
.Filter(p => p.ScheduleKind == PlayoutScheduleKind.Block ||
|
||||
p.ScheduleKind == PlayoutScheduleKind.Sequential)
|
||||
p.ScheduleKind == PlayoutScheduleKind.Sequential ||
|
||||
p.ScheduleKind == PlayoutScheduleKind.Scripted)
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
|
||||
|
||||
foreach (Playout playout in maybePlayout)
|
||||
|
||||
@@ -17,7 +17,8 @@ public class ErasePlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFact
|
||||
.Include(p => p.Items)
|
||||
.Include(p => p.PlayoutHistory)
|
||||
.Filter(p => p.ScheduleKind == PlayoutScheduleKind.Block ||
|
||||
p.ScheduleKind == PlayoutScheduleKind.Sequential)
|
||||
p.ScheduleKind == PlayoutScheduleKind.Sequential ||
|
||||
p.ScheduleKind == PlayoutScheduleKind.Scripted)
|
||||
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
|
||||
|
||||
foreach (Playout playout in maybePlayout)
|
||||
|
||||
@@ -68,7 +68,12 @@ public class PreviewBlockPlayoutHandler(
|
||||
playout.PlayoutHistory.ToList());
|
||||
|
||||
PlayoutBuildResult result =
|
||||
await blockPlayoutBuilder.Build(playout, referenceData, PlayoutBuildMode.Reset, cancellationToken);
|
||||
await blockPlayoutBuilder.Build(
|
||||
DateTimeOffset.Now,
|
||||
playout,
|
||||
referenceData,
|
||||
PlayoutBuildMode.Reset,
|
||||
cancellationToken);
|
||||
|
||||
// load playout item details for title
|
||||
foreach (PlayoutItem playoutItem in result.AddedItems)
|
||||
|
||||
@@ -47,4 +47,5 @@ public class ConfigElementKey
|
||||
public static ConfigElementKey PlayoutSkipMissingItems => new("playout.skip_missing_items");
|
||||
public static ConfigElementKey XmltvTimeZone => new("xmltv.time_zone");
|
||||
public static ConfigElementKey XmltvDaysToBuild => new("xmltv.days_to_build");
|
||||
public static ConfigElementKey XmltvBlockBehavior => new("xmltv.block_behavior");
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ public class Playout
|
||||
public Channel Channel { get; set; }
|
||||
public int? ProgramScheduleId { get; set; }
|
||||
public ProgramSchedule ProgramSchedule { get; set; }
|
||||
public string ExternalJsonFile { get; set; }
|
||||
public string TemplateFile { get; set; }
|
||||
public string ScheduleFile { get; set; }
|
||||
public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
|
||||
public PlayoutScheduleKind ScheduleKind { get; set; }
|
||||
public List<PlayoutItem> Items { get; set; }
|
||||
|
||||
@@ -6,6 +6,7 @@ public enum PlayoutScheduleKind
|
||||
Classic = 1,
|
||||
Block = 2,
|
||||
Sequential = 3,
|
||||
Scripted = 4,
|
||||
|
||||
ExternalJson = 20
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<PackageReference Include="Bugsnag" Version="4.1.0" />
|
||||
<PackageReference Include="Destructurama.Attributed" Version="5.1.0" />
|
||||
<PackageReference Include="Flurl" Version="4.0.0" />
|
||||
<PackageReference Include="IronPython" Version="3.4.2" />
|
||||
<PackageReference Include="IronPython.StdLib" Version="3.4.2" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.4.9" />
|
||||
<PackageReference Include="LanguageExt.Transformers" Version="4.4.8" />
|
||||
<PackageReference Include="MediatR" Version="[12.5.0]" />
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
|
||||
public interface IBlockPlayoutBuilder
|
||||
{
|
||||
Task<PlayoutBuildResult> Build(
|
||||
DateTimeOffset start,
|
||||
Playout playout,
|
||||
PlayoutReferenceData referenceData,
|
||||
PlayoutBuildMode mode,
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
public interface IYamlPlayoutBuilder
|
||||
public interface IScriptedPlayoutBuilder
|
||||
{
|
||||
Task<PlayoutBuildResult> Build(
|
||||
DateTimeOffset start,
|
||||
@@ -0,0 +1,14 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
public interface ISequentialPlayoutBuilder
|
||||
{
|
||||
Task<PlayoutBuildResult> Build(
|
||||
DateTimeOffset start,
|
||||
Playout playout,
|
||||
PlayoutReferenceData referenceData,
|
||||
PlayoutBuildMode mode,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public class BlockPlayoutBuilder(
|
||||
protected virtual ILogger Logger => logger;
|
||||
|
||||
public virtual async Task<PlayoutBuildResult> Build(
|
||||
DateTimeOffset start,
|
||||
Playout playout,
|
||||
PlayoutReferenceData referenceData,
|
||||
PlayoutBuildMode mode,
|
||||
@@ -50,8 +51,6 @@ public class BlockPlayoutBuilder(
|
||||
PlaybackOrder.RandomRotation
|
||||
];
|
||||
|
||||
DateTimeOffset start = DateTimeOffset.Now;
|
||||
|
||||
int daysToBuild = await GetDaysToBuild();
|
||||
|
||||
// get blocks to schedule
|
||||
@@ -78,9 +77,9 @@ public class BlockPlayoutBuilder(
|
||||
result.ItemsToRemove.Add(item.Id);
|
||||
}
|
||||
|
||||
// remove old items
|
||||
// importantly, this should not remove their history
|
||||
result = result with { RemoveBefore = start };
|
||||
// // remove old items
|
||||
// // importantly, this should not remove their history
|
||||
// result = result with { RemoveBefore = start };
|
||||
|
||||
(List<EffectiveBlock> updatedEffectiveBlocks, List<PlayoutItem> playoutItemsToRemove) =
|
||||
BlockPlayoutChangeDetection.FindUpdatedItems(
|
||||
|
||||
@@ -27,6 +27,7 @@ public class BlockPlayoutPreviewBuilder(
|
||||
protected override ILogger Logger => NullLogger.Instance;
|
||||
|
||||
public override async Task<PlayoutBuildResult> Build(
|
||||
DateTimeOffset start,
|
||||
Playout playout,
|
||||
PlayoutReferenceData referenceData,
|
||||
PlayoutBuildMode mode,
|
||||
@@ -34,7 +35,7 @@ public class BlockPlayoutPreviewBuilder(
|
||||
{
|
||||
_randomizedCollections.Add(playout.Channel.UniqueId, []);
|
||||
|
||||
PlayoutBuildResult result = await base.Build(playout, referenceData, mode, cancellationToken);
|
||||
PlayoutBuildResult result = await base.Build(start, playout, referenceData, mode, cancellationToken);
|
||||
|
||||
_randomizedCollections.Remove(playout.Channel.UniqueId);
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
|
||||
|
||||
[SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores")]
|
||||
public class ContentModule
|
||||
{
|
||||
private Dictionary<string, IMediaCollectionEnumerator> _contentEnumerators = [];
|
||||
|
||||
public bool add_collection(string key, string name, string order)
|
||||
{
|
||||
if (_contentEnumerators.ContainsKey(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Adding collection '{name}' with key '{key}' and order '{order}'");
|
||||
_contentEnumerators.Clear();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling.ScriptedScheduling.Modules;
|
||||
using IronPython.Hosting;
|
||||
using IronPython.Runtime;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.ScriptedScheduling;
|
||||
|
||||
public class ScriptedPlayoutBuilder(
|
||||
//IConfigElementRepository configElementRepository,
|
||||
//IMediaCollectionRepository mediaCollectionRepository,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<ScriptedPlayoutBuilder> logger)
|
||||
: IScriptedPlayoutBuilder
|
||||
{
|
||||
public async Task<PlayoutBuildResult> Build(
|
||||
DateTimeOffset start,
|
||||
Playout playout,
|
||||
PlayoutReferenceData referenceData,
|
||||
PlayoutBuildMode mode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(10, cancellationToken);
|
||||
|
||||
var result = PlayoutBuildResult.Empty;
|
||||
|
||||
if (!localFileSystem.FileExists(playout.ScheduleFile))
|
||||
{
|
||||
logger.LogError("Cannot build scripted playout; schedule file {File} does not exist", playout.ScheduleFile);
|
||||
return result;
|
||||
}
|
||||
|
||||
logger.LogInformation("Building scripted playout...");
|
||||
|
||||
//int daysToBuild = await GetDaysToBuild();
|
||||
//DateTimeOffset finish = start.AddDays(daysToBuild);
|
||||
|
||||
//var enumeratorCache = new EnumeratorCache(mediaCollectionRepository, logger);
|
||||
|
||||
// apply all history???
|
||||
|
||||
try
|
||||
{
|
||||
var engine = Python.CreateEngine();
|
||||
var scope = engine.CreateScope();
|
||||
|
||||
var sys = engine.GetSysModule();
|
||||
var modules = (PythonDictionary)sys.GetVariable("modules");
|
||||
|
||||
var types = engine.ImportModule("types");
|
||||
dynamic moduleType = types.GetVariable("ModuleType");
|
||||
|
||||
dynamic ersatztv = engine.Operations.Invoke(moduleType, "ersatztv");
|
||||
modules["ersatztv"] = ersatztv;
|
||||
|
||||
var contentModule = new ContentModule();
|
||||
engine.Operations.SetMember(ersatztv, "content", contentModule);
|
||||
|
||||
engine.ExecuteFile(playout.ScheduleFile, scope);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Unexpected error building scripted playout");
|
||||
throw;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// private async Task<int> GetDaysToBuild() =>
|
||||
// await configElementRepository
|
||||
// .GetValue<int>(ConfigElementKey.PlayoutDaysToBuild)
|
||||
// .IfNoneAsync(2);
|
||||
}
|
||||
@@ -12,6 +12,6 @@ public interface IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutAllInstruction all)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache)
|
||||
IReadOnlyCollection<PlayoutHistory> filteredHistory,
|
||||
YamlPlayoutContext context,
|
||||
YamlPlayoutContentItem contentItem,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentItem.Key))
|
||||
|
||||
@@ -17,13 +17,13 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
protected async Task<Option<IMediaCollectionEnumerator>> GetContentEnumerator(
|
||||
YamlPlayoutContext context,
|
||||
string contentKey,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(contentKey))
|
||||
@@ -54,7 +54,7 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache)
|
||||
IMediaCollectionEnumerator enumerator,
|
||||
PlayoutItem playoutItem,
|
||||
MediaItem mediaItem,
|
||||
ILogger<YamlPlayoutBuilder> logger)
|
||||
ILogger<SequentialPlayoutBuilder> logger)
|
||||
{
|
||||
int index = context.Definition.Content.FindIndex(c => c.Key == contentKey);
|
||||
if (index < 0)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutCountInstruction count)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutDurationInstruction duration)
|
||||
@@ -89,7 +89,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP
|
||||
IMediaCollectionEnumerator enumerator,
|
||||
Option<IMediaCollectionEnumerator> fallbackEnumerator,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger)
|
||||
ILogger<SequentialPlayoutBuilder> logger)
|
||||
{
|
||||
var done = false;
|
||||
TimeSpan remainingToFill = targetTime - context.CurrentTime;
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutEpgGroupHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutEpgGroupInstruction epgGroup)
|
||||
|
||||
@@ -16,7 +16,7 @@ public class YamlPlayoutGraphicsOffHandler(IGraphicsElementRepository graphicsEl
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutGraphicsOffInstruction graphicsOff)
|
||||
|
||||
@@ -22,7 +22,7 @@ public class YamlPlayoutGraphicsOnHandler(IGraphicsElementRepository graphicsEle
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutGraphicsOnInstruction graphicsOn)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutMidRollHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutMidRollInstruction midRoll)
|
||||
|
||||
@@ -11,7 +11,7 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutPadToNextInstruction padToNext)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutPadUntilInstruction padUntil)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutPostRollHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutPostRollInstruction postRoll)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutPreRollHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutPreRollInstruction preRoll)
|
||||
|
||||
@@ -14,7 +14,7 @@ public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutRepeatInstruction)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutRewindHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutRewindInstruction rewind)
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ public class YamlPlayoutShuffleSequenceHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutShuffleSequenceInstruction shuffleSequenceInstruction)
|
||||
|
||||
@@ -14,7 +14,7 @@ public class YamlPlayoutSkipItemsHandler(EnumeratorCache enumeratorCache) : IYam
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutSkipItemsInstruction skipItems)
|
||||
|
||||
@@ -14,7 +14,7 @@ public class YamlPlayoutSkipToItemHandler(EnumeratorCache enumeratorCache) : IYa
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutSkipToItemInstruction skipToItem)
|
||||
|
||||
@@ -12,7 +12,7 @@ public class YamlPlayoutWaitUntilHandler : IYamlPlayoutHandler
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutWaitUntilInstruction waitUntil)
|
||||
|
||||
@@ -16,7 +16,7 @@ public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) :
|
||||
YamlPlayoutInstruction instruction,
|
||||
PlayoutBuildMode mode,
|
||||
Func<string, Task> executeSequence,
|
||||
ILogger<YamlPlayoutBuilder> logger,
|
||||
ILogger<SequentialPlayoutBuilder> logger,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (instruction is not YamlPlayoutWatermarkInstruction watermark)
|
||||
|
||||
+8
-8
@@ -14,15 +14,15 @@ using YamlDotNet.Serialization.NamingConventions;
|
||||
|
||||
namespace ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
|
||||
public class YamlPlayoutBuilder(
|
||||
public class SequentialPlayoutBuilder(
|
||||
ILocalFileSystem localFileSystem,
|
||||
IConfigElementRepository configElementRepository,
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IChannelRepository channelRepository,
|
||||
IGraphicsElementRepository graphicsElementRepository,
|
||||
ISequentialScheduleValidator sequentialScheduleValidator,
|
||||
ILogger<YamlPlayoutBuilder> logger)
|
||||
: IYamlPlayoutBuilder
|
||||
ILogger<SequentialPlayoutBuilder> logger)
|
||||
: ISequentialPlayoutBuilder
|
||||
{
|
||||
public async Task<PlayoutBuildResult> Build(
|
||||
DateTimeOffset start,
|
||||
@@ -33,17 +33,17 @@ public class YamlPlayoutBuilder(
|
||||
{
|
||||
PlayoutBuildResult result = PlayoutBuildResult.Empty;
|
||||
|
||||
if (!localFileSystem.FileExists(playout.TemplateFile))
|
||||
if (!localFileSystem.FileExists(playout.ScheduleFile))
|
||||
{
|
||||
logger.LogWarning("YAML playout file {File} does not exist; aborting.", playout.TemplateFile);
|
||||
logger.LogWarning("Sequential schedule file {File} does not exist; aborting.", playout.ScheduleFile);
|
||||
return result;
|
||||
}
|
||||
|
||||
Option<YamlPlayoutDefinition> maybePlayoutDefinition =
|
||||
await LoadYamlDefinition(playout.TemplateFile, false, cancellationToken);
|
||||
await LoadYamlDefinition(playout.ScheduleFile, false, cancellationToken);
|
||||
if (maybePlayoutDefinition.IsNone)
|
||||
{
|
||||
logger.LogWarning("YAML playout file {File} is invalid; aborting.", playout.TemplateFile);
|
||||
logger.LogWarning("Sequential schedule file {File} is invalid; aborting.", playout.ScheduleFile);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class YamlPlayoutBuilder(
|
||||
if (!File.Exists(import))
|
||||
{
|
||||
path = Path.Combine(
|
||||
Path.GetDirectoryName(playout.TemplateFile) ?? string.Empty,
|
||||
Path.GetDirectoryName(playout.ScheduleFile) ?? string.Empty,
|
||||
import ?? string.Empty);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Generated
+6334
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutScheduleFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ScheduleFile",
|
||||
table: "Playout",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.Sql("UPDATE `Playout` SET `ScheduleFile` = COALESCE(`ExternalJsonFile`, `TemplateFile`)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ScheduleFile",
|
||||
table: "Playout");
|
||||
}
|
||||
}
|
||||
}
|
||||
+6328
File diff suppressed because it is too large
Load Diff
+40
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Remove_PlayoutExternalJsonFileTemplateFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ExternalJsonFile",
|
||||
table: "Playout");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TemplateFile",
|
||||
table: "Playout");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ExternalJsonFile",
|
||||
table: "Playout",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TemplateFile",
|
||||
table: "Playout",
|
||||
type: "longtext",
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1822,24 +1822,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
|
||||
b.Property<int?>("DecoId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ExternalJsonFile")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<DateTimeOffset?>("OnDemandCheckpoint")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("ProgramScheduleId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ScheduleFile")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<int>("ScheduleKind")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("Seed")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("TemplateFile")
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
Generated
+6169
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Add_PlayoutScheduleFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ScheduleFile",
|
||||
table: "Playout",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.Sql("UPDATE `Playout` SET `ScheduleFile` = COALESCE(`ExternalJsonFile`, `TemplateFile`)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ScheduleFile",
|
||||
table: "Playout");
|
||||
}
|
||||
}
|
||||
}
|
||||
+6163
File diff suppressed because it is too large
Load Diff
+38
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Remove_PlayoutExternalJsonFileTemplateFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ExternalJsonFile",
|
||||
table: "Playout");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TemplateFile",
|
||||
table: "Playout");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ExternalJsonFile",
|
||||
table: "Playout",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TemplateFile",
|
||||
table: "Playout",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1733,24 +1733,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
|
||||
b.Property<int?>("DecoId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ExternalJsonFile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset?>("OnDemandCheckpoint")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ProgramScheduleId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("ScheduleFile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("ScheduleKind")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Seed")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("TemplateFile")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ChannelId");
|
||||
|
||||
@@ -61,14 +61,14 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
|
||||
if (playout.ScheduleKind == PlayoutScheduleKind.ExternalJson)
|
||||
{
|
||||
// json file must exist
|
||||
if (_localFileSystem.FileExists(playout.ExternalJsonFile))
|
||||
if (_localFileSystem.FileExists(playout.ScheduleFile))
|
||||
{
|
||||
return await GetExternalJsonPlayoutItem(dbContext, playout, now, ffprobePath);
|
||||
}
|
||||
|
||||
_logger.LogWarning(
|
||||
"Unable to locate external json file {File} for channel {Number} - {Name}",
|
||||
playout.ExternalJsonFile,
|
||||
"Unable to locate json schedule file {File} for channel {Number} - {Name}",
|
||||
playout.ScheduleFile,
|
||||
channel.Number,
|
||||
channel.Name);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
|
||||
string ffprobePath)
|
||||
{
|
||||
Option<ExternalJsonChannel> maybeChannel = JsonConvert.DeserializeObject<ExternalJsonChannel>(
|
||||
await File.ReadAllTextAsync(playout.ExternalJsonFile));
|
||||
await File.ReadAllTextAsync(playout.ScheduleFile));
|
||||
|
||||
// must deserialize channel from json
|
||||
foreach (ExternalJsonChannel channel in maybeChannel)
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerCh
|
||||
return new NotFoundResult();
|
||||
}
|
||||
|
||||
// for debugging by fast-forwarding a YAML playout
|
||||
// for debugging by fast-forwarding a playout
|
||||
// [HttpPost("/api/channels/{channelNumber}/playout/continue")]
|
||||
// public async Task<IActionResult> ContinuePlayout(string channelNumber)
|
||||
// {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="d-flex">
|
||||
<MudText>JSON (dizqueTV) Schedule</MudText>
|
||||
</div>
|
||||
<MudTextField @bind-Value="_model.ExternalJsonFile" For="@(() => _model.ExternalJsonFile)"/>
|
||||
<MudTextField @bind-Value="_model.ScheduleFile" For="@(() => _model.ScheduleFile)"/>
|
||||
</MudStack>
|
||||
break;
|
||||
case PlayoutKind.Sequential:
|
||||
@@ -49,7 +49,15 @@
|
||||
<div class="d-flex">
|
||||
<MudText>Sequential Schedule</MudText>
|
||||
</div>
|
||||
<MudTextField @bind-Value="_model.SequentialSchedule" For="@(() => _model.SequentialSchedule)"/>
|
||||
<MudTextField @bind-Value="_model.ScheduleFile" For="@(() => _model.ScheduleFile)"/>
|
||||
</MudStack>
|
||||
break;
|
||||
case PlayoutKind.Scripted:
|
||||
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
|
||||
<div class="d-flex">
|
||||
<MudText>Scripted Schedule</MudText>
|
||||
</div>
|
||||
<MudTextField @bind-Value="_model.ScheduleFile" For="@(() => _model.ScheduleFile)"/>
|
||||
</MudStack>
|
||||
break;
|
||||
case PlayoutKind.Block:
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Classic Schedule" Href="playouts/add"/>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Block Schedule" Href="@($"playouts/add/{PlayoutKind.Block}")"/>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Sequential Schedule" Href="@($"playouts/add/{PlayoutKind.Sequential}")"/>
|
||||
@* <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Scripted Schedule" Href="@($"playouts/add/{PlayoutKind.Scripted}")"/> *@
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="JSON (dizqueTV) Schedule" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")"/>
|
||||
</ChildContent>
|
||||
</MudMenu>
|
||||
@@ -38,6 +39,7 @@
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Classic Playout" Href="playouts/add"/>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Block Playout" Href="@($"playouts/add/{PlayoutKind.Block}")"/>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Sequential Playout" Href="@($"playouts/add/{PlayoutKind.Sequential}")"/>
|
||||
@* <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Scripted Playout" Href="@($"playouts/add/{PlayoutKind.Scripted}")"/> *@
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add JSON (dizqueTV) Playout" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")"/>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Refresh" Label="Reset All Playouts" OnClick="@ResetAllPlayouts"/>
|
||||
</MudMenu>
|
||||
@@ -88,6 +90,9 @@
|
||||
case PlayoutScheduleKind.Sequential:
|
||||
<span>Sequential</span>
|
||||
break;
|
||||
case PlayoutScheduleKind.Scripted:
|
||||
<span>Scripted</span>
|
||||
break;
|
||||
case PlayoutScheduleKind.ExternalJson:
|
||||
<span>JSON (dizqueTV)</span>
|
||||
break;
|
||||
@@ -153,7 +158,23 @@
|
||||
<MudTooltip Text="Edit Playout">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
Href="@($"playouts/yaml/{context.PlayoutId}")">
|
||||
Href="@($"playouts/sequential/{context.PlayoutId}")">
|
||||
</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.ScheduleKind == PlayoutScheduleKind.Scripted)
|
||||
{
|
||||
<MudTooltip Text="Edit Playout">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Disabled="@EntityLocker.IsPlayoutLocked(context.PlayoutId)"
|
||||
Href="@($"playouts/scripted/{context.PlayoutId}")">
|
||||
</MudIconButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Reset Playout">
|
||||
@@ -290,8 +311,8 @@
|
||||
|
||||
private async Task PlayoutSelected(PlayoutNameViewModel playout)
|
||||
{
|
||||
// only show details for flood, block and sequential playouts
|
||||
_selectedPlayoutId = playout.ScheduleKind is PlayoutScheduleKind.Classic or PlayoutScheduleKind.Block or PlayoutScheduleKind.Sequential
|
||||
// only show details for classic, block, sequential and scripted schedules
|
||||
_selectedPlayoutId = playout.ScheduleKind is PlayoutScheduleKind.Classic or PlayoutScheduleKind.Block or PlayoutScheduleKind.Sequential or PlayoutScheduleKind.Scripted
|
||||
? playout.PlayoutId
|
||||
: null;
|
||||
|
||||
@@ -303,14 +324,14 @@
|
||||
|
||||
private async Task EditExternalJsonFile(PlayoutNameViewModel playout)
|
||||
{
|
||||
var parameters = new DialogParameters { { "ExternalJsonFile", $"{playout.ExternalJsonFile}" } };
|
||||
var parameters = new DialogParameters { { "ScheduleFile", $"{playout.ScheduleFile}" } };
|
||||
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraLarge };
|
||||
|
||||
IDialogReference dialog = await Dialog.ShowAsync<EditExternalJsonFileDialog>("Edit External Json File", parameters, options);
|
||||
DialogResult result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
await Mediator.Send(new UpdateExternalJsonPlayout(playout.PlayoutId, result.Data as string ?? playout.ExternalJsonFile), _cts.Token);
|
||||
await Mediator.Send(new UpdateExternalJsonPlayout(playout.PlayoutId, result.Data as string ?? playout.ScheduleFile), _cts.Token);
|
||||
if (_table != null)
|
||||
{
|
||||
await _table.ReloadServerData();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@page "/playouts/yaml/{Id:int}"
|
||||
@page "/playouts/sequential/{Id:int}"
|
||||
@using ErsatzTV.Application.Channels
|
||||
@using ErsatzTV.Application.Playouts
|
||||
@using ErsatzTV.Application.Scheduling
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="d-flex">
|
||||
<MudText>Sequential Schedule</MudText>
|
||||
</div>
|
||||
<MudTextField @bind-Value="@_playout.TemplateFile" For="@(() => _playout.TemplateFile)"/>
|
||||
<MudTextField @bind-Value="@_playout.ScheduleFile" For="@(() => _playout.ScheduleFile)"/>
|
||||
</MudStack>
|
||||
<MudText Typo="Typo.h5" Class="mt-10 mb-2">Maintenance</MudText>
|
||||
<MudDivider Class="mb-6"/>
|
||||
@@ -92,7 +92,7 @@
|
||||
}
|
||||
|
||||
Either<BaseError, PlayoutNameViewModel> result =
|
||||
await Mediator.Send(new UpdateSequentialPlayout(_playout.PlayoutId, _playout.TemplateFile), _cts.Token);
|
||||
await Mediator.Send(new UpdateSequentialPlayout(_playout.PlayoutId, _playout.ScheduleFile), _cts.Token);
|
||||
|
||||
result.Match(
|
||||
_ => { Snackbar.Add($"Saved sequential schedule for playout {_channelName}", Severity.Success); },
|
||||
|
||||
@@ -28,6 +28,15 @@
|
||||
<MudSelectItem Value="@XmltvTimeZone.Utc">UTC</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
|
||||
<div class="d-flex">
|
||||
<MudText>XMLTV Block Behavior</MudText>
|
||||
</div>
|
||||
<MudSelect @bind-Value="_xmltvSettings.BlockBehavior">
|
||||
<MudSelectItem Value="@XmltvBlockBehavior.SplitTimeEvenly">Split Time Evenly</MudSelectItem>
|
||||
<MudSelectItem Value="@XmltvBlockBehavior.UseActualTimes">Use Actual Times</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
</MudContainer>
|
||||
</div>
|
||||
</MudForm>
|
||||
|
||||
@@ -4,5 +4,6 @@ public static class PlayoutKind
|
||||
{
|
||||
public const string ExternalJson = "externaljson";
|
||||
public const string Sequential = "sequential";
|
||||
public const string Scripted = "scripted";
|
||||
public const string Block = "block";
|
||||
}
|
||||
|
||||
+3
-1
@@ -37,6 +37,7 @@ using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Plex;
|
||||
using ErsatzTV.Core.Scheduling;
|
||||
using ErsatzTV.Core.Scheduling.BlockScheduling;
|
||||
using ErsatzTV.Core.Scheduling.ScriptedScheduling;
|
||||
using ErsatzTV.Core.Scheduling.YamlScheduling;
|
||||
using ErsatzTV.Core.Search;
|
||||
using ErsatzTV.Core.Trakt;
|
||||
@@ -695,7 +696,8 @@ public class Startup
|
||||
services.AddScoped<IBlockPlayoutBuilder, BlockPlayoutBuilder>();
|
||||
services.AddScoped<IBlockPlayoutPreviewBuilder, BlockPlayoutPreviewBuilder>();
|
||||
services.AddScoped<IBlockPlayoutFillerBuilder, BlockPlayoutFillerBuilder>();
|
||||
services.AddScoped<IYamlPlayoutBuilder, YamlPlayoutBuilder>();
|
||||
services.AddScoped<ISequentialPlayoutBuilder, SequentialPlayoutBuilder>();
|
||||
services.AddScoped<IScriptedPlayoutBuilder, ScriptedPlayoutBuilder>();
|
||||
services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>();
|
||||
services.AddScoped<IPlayoutTimeShifter, PlayoutTimeShifter>();
|
||||
services.AddScoped<IImageCache, ImageCache>();
|
||||
|
||||
@@ -10,7 +10,8 @@ public class PlayoutEditViewModelValidator : AbstractValidator<PlayoutEditViewMo
|
||||
{
|
||||
RuleFor(p => p.Channel).NotNull();
|
||||
RuleFor(p => p.ProgramSchedule).NotNull().When(p => string.IsNullOrWhiteSpace(p.Kind));
|
||||
RuleFor(p => p.ExternalJsonFile).NotNull().When(p => p.Kind == PlayoutKind.ExternalJson);
|
||||
RuleFor(p => p.ScheduleFile).NotNull().When(p =>
|
||||
p.Kind is PlayoutKind.ExternalJson or PlayoutKind.Sequential or PlayoutKind.Scripted);
|
||||
}
|
||||
|
||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||
|
||||
@@ -9,14 +9,14 @@ public class PlayoutEditViewModel
|
||||
public string Kind { get; set; }
|
||||
public ChannelViewModel Channel { get; set; }
|
||||
public ProgramScheduleViewModel ProgramSchedule { get; set; }
|
||||
public string ExternalJsonFile { get; set; }
|
||||
public string SequentialSchedule { get; set; }
|
||||
public string ScheduleFile { get; set; }
|
||||
|
||||
public CreatePlayout ToCreate() =>
|
||||
Kind switch
|
||||
{
|
||||
PlayoutKind.ExternalJson => new CreateExternalJsonPlayout(Channel.Id, ExternalJsonFile),
|
||||
PlayoutKind.Sequential => new CreateSequentialPlayout(Channel.Id, SequentialSchedule),
|
||||
PlayoutKind.ExternalJson => new CreateExternalJsonPlayout(Channel.Id, ScheduleFile),
|
||||
PlayoutKind.Sequential => new CreateSequentialPlayout(Channel.Id, ScheduleFile),
|
||||
PlayoutKind.Scripted => new CreateScriptedPlayout(Channel.Id, ScheduleFile),
|
||||
PlayoutKind.Block => new CreateBlockPlayout(Channel.Id),
|
||||
_ => new CreateClassicPlayout(Channel.Id, ProgramSchedule.Id)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user