Browse Source

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
pull/2339/head
Jason Dove 11 months ago committed by GitHub
parent
commit
53f281ce32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      CHANGELOG.md
  2. 119
      ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs
  3. 1
      ErsatzTV.Application/Configuration/Commands/UpdateXmltvSettingsHandler.cs
  4. 6
      ErsatzTV.Application/Configuration/Queries/GetXmltvSettingsHandler.cs
  5. 7
      ErsatzTV.Application/Configuration/XmltvBlockBehavior.cs
  6. 1
      ErsatzTV.Application/Configuration/XmltvSettingsViewModel.cs
  7. 26
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  8. 6
      ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs
  9. 7
      ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs
  10. 96
      ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs
  11. 10
      ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs
  12. 1
      ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs
  13. 2
      ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayout.cs
  14. 5
      ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs
  15. 3
      ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs
  16. 2
      ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayout.cs
  17. 5
      ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs
  18. 5
      ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
  19. 3
      ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs
  20. 3
      ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs
  21. 3
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs
  22. 3
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutItemsHandler.cs
  23. 7
      ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs
  24. 1
      ErsatzTV.Core/Domain/ConfigElementKey.cs
  25. 3
      ErsatzTV.Core/Domain/Playout.cs
  26. 1
      ErsatzTV.Core/Domain/PlayoutScheduleKind.cs
  27. 2
      ErsatzTV.Core/ErsatzTV.Core.csproj
  28. 1
      ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs
  29. 2
      ErsatzTV.Core/Interfaces/Scheduling/IScriptedPlayoutBuilder.cs
  30. 14
      ErsatzTV.Core/Interfaces/Scheduling/ISequentialPlayoutBuilder.cs
  31. 9
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs
  32. 3
      ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs
  33. 23
      ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/ContentModule.cs
  34. 76
      ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs
  35. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/IYamlPlayoutHandler.cs
  36. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs
  37. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs
  38. 6
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs
  39. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs
  40. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs
  41. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutEpgGroupHandler.cs
  42. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOffHandler.cs
  43. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOnHandler.cs
  44. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutMidRollHandler.cs
  45. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs
  46. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs
  47. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPostRollHandler.cs
  48. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPreRollHandler.cs
  49. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs
  50. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRewindHandler.cs
  51. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutShuffleSequenceHandler.cs
  52. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs
  53. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipToItemHandler.cs
  54. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWaitUntilHandler.cs
  55. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWatermarkHandler.cs
  56. 16
      ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs
  57. 6334
      ErsatzTV.Infrastructure.MySql/Migrations/20250823155914_Add_PlayoutScheduleFile.Designer.cs
  58. 31
      ErsatzTV.Infrastructure.MySql/Migrations/20250823155914_Add_PlayoutScheduleFile.cs
  59. 6328
      ErsatzTV.Infrastructure.MySql/Migrations/20250823160902_Remove_PlayoutExternalJsonFileTemplateFile.Designer.cs
  60. 40
      ErsatzTV.Infrastructure.MySql/Migrations/20250823160902_Remove_PlayoutExternalJsonFileTemplateFile.cs
  61. 9
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  62. 6169
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250823155845_Add_PlayoutScheduleFile.Designer.cs
  63. 30
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250823155845_Add_PlayoutScheduleFile.cs
  64. 6163
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250823160940_Remove_PlayoutExternalJsonFileTemplateFile.Designer.cs
  65. 38
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250823160940_Remove_PlayoutExternalJsonFileTemplateFile.cs
  66. 9
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  67. 8
      ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs
  68. 2
      ErsatzTV/Controllers/Api/ChannelController.cs
  69. 12
      ErsatzTV/Pages/PlayoutEditor.razor
  70. 31
      ErsatzTV/Pages/Playouts.razor
  71. 6
      ErsatzTV/Pages/SequentialPlayoutEditor.razor
  72. 9
      ErsatzTV/Pages/Settings/XMLTVSettings.razor
  73. 1
      ErsatzTV/PlayoutKind.cs
  74. 4
      ErsatzTV/Startup.cs
  75. 3
      ErsatzTV/Validators/PlayoutEditViewModelValidator.cs
  76. 8
      ErsatzTV/ViewModels/PlayoutEditViewModel.cs

4
CHANGELOG.md

@ -57,6 +57,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -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

119
ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs

@ -194,6 +194,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData> @@ -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> @@ -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> @@ -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()
};
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);
}
break;
case XmltvBlockBehavior.SplitTimeEvenly:
default:
DateTime groupStart = group.Key.GuideStart!.Value;
DateTime groupFinish = group.Key.GuideFinish!.Value;
TimeSpan groupDuration = groupFinish - groupStart;
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;
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;
}
}
}

1
ErsatzTV.Application/Configuration/Commands/UpdateXmltvSettingsHandler.cs

@ -36,6 +36,7 @@ public class UpdateXmltvSettingsHandler( @@ -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();

6
ErsatzTV.Application/Configuration/Queries/GetXmltvSettingsHandler.cs

@ -13,10 +13,14 @@ public class GetXmltvSettingsHandler(IConfigElementRepository configElementRepos @@ -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)
};
}
}

7
ErsatzTV.Application/Configuration/XmltvBlockBehavior.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Application.Configuration;
public enum XmltvBlockBehavior
{
SplitTimeEvenly = 0,
UseActualTimes = 1
}

1
ErsatzTV.Application/Configuration/XmltvSettingsViewModel.cs

@ -4,4 +4,5 @@ public class XmltvSettingsViewModel @@ -4,4 +4,5 @@ public class XmltvSettingsViewModel
{
public int DaysToBuild { get; set; }
public XmltvTimeZone TimeZone { get; set; }
public XmltvBlockBehavior BlockBehavior { get; set; }
}

26
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -29,7 +29,8 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -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 @@ -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 @@ -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 @@ -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 @@ -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,

6
ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs

@ -53,7 +53,7 @@ public class CreateExternalJsonPlayoutHandler @@ -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 @@ -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(

7
ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs

@ -12,8 +12,11 @@ public record CreateClassicPlayout(int ChannelId, int ProgramScheduleId) @@ -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);

96
ErsatzTV.Application/Playouts/Commands/CreateScriptedPlayoutHandler.cs

@ -0,0 +1,96 @@ @@ -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");
}

10
ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs

@ -58,7 +58,7 @@ public class CreateSequentialPlayoutHandler @@ -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 @@ -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");
}

1
ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs

@ -24,6 +24,7 @@ public class ResetAllPlayoutsHandler( @@ -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
ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayout.cs

@ -2,5 +2,5 @@ @@ -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>>;

5
ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs

@ -37,7 +37,7 @@ public class @@ -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 @@ -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);
}

3
ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs

@ -43,8 +43,7 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr @@ -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
ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayout.cs

@ -2,5 +2,5 @@ @@ -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>>;

5
ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs

@ -37,7 +37,7 @@ public class @@ -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 @@ -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);
}

5
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs

@ -9,11 +9,10 @@ public record PlayoutNameViewModel( @@ -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;
}

3
ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs

@ -26,8 +26,7 @@ public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<Playou @@ -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);
}

3
ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs

@ -24,8 +24,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory @@ -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));
}
}

3
ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs

@ -14,7 +14,8 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa @@ -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)

3
ErsatzTV.Application/Scheduling/Commands/ErasePlayoutItemsHandler.cs

@ -17,7 +17,8 @@ public class ErasePlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFact @@ -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)

7
ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs

@ -68,7 +68,12 @@ public class PreviewBlockPlayoutHandler( @@ -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)

1
ErsatzTV.Core/Domain/ConfigElementKey.cs

@ -47,4 +47,5 @@ public class ConfigElementKey @@ -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");
}

3
ErsatzTV.Core/Domain/Playout.cs

@ -9,8 +9,7 @@ public class Playout @@ -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; }

1
ErsatzTV.Core/Domain/PlayoutScheduleKind.cs

@ -6,6 +6,7 @@ public enum PlayoutScheduleKind @@ -6,6 +6,7 @@ public enum PlayoutScheduleKind
Classic = 1,
Block = 2,
Sequential = 3,
Scripted = 4,
ExternalJson = 20
}

2
ErsatzTV.Core/ErsatzTV.Core.csproj

@ -12,6 +12,8 @@ @@ -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]" />

1
ErsatzTV.Core/Interfaces/Scheduling/IBlockPlayoutBuilder.cs

@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling; @@ -6,6 +6,7 @@ namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IBlockPlayoutBuilder
{
Task<PlayoutBuildResult> Build(
DateTimeOffset start,
Playout playout,
PlayoutReferenceData referenceData,
PlayoutBuildMode mode,

2
ErsatzTV.Core/Interfaces/Scheduling/IYamlPlayoutBuilder.cs → ErsatzTV.Core/Interfaces/Scheduling/IScriptedPlayoutBuilder.cs

@ -3,7 +3,7 @@ using ErsatzTV.Core.Scheduling; @@ -3,7 +3,7 @@ using ErsatzTV.Core.Scheduling;
namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IYamlPlayoutBuilder
public interface IScriptedPlayoutBuilder
{
Task<PlayoutBuildResult> Build(
DateTimeOffset start,

14
ErsatzTV.Core/Interfaces/Scheduling/ISequentialPlayoutBuilder.cs

@ -0,0 +1,14 @@ @@ -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);
}

9
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutBuilder.cs

@ -28,6 +28,7 @@ public class BlockPlayoutBuilder( @@ -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( @@ -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( @@ -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(

3
ErsatzTV.Core/Scheduling/BlockScheduling/BlockPlayoutPreviewBuilder.cs

@ -27,6 +27,7 @@ public class BlockPlayoutPreviewBuilder( @@ -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( @@ -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);

23
ErsatzTV.Core/Scheduling/ScriptedScheduling/Modules/ContentModule.cs

@ -0,0 +1,23 @@ @@ -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;
}
}

76
ErsatzTV.Core/Scheduling/ScriptedScheduling/ScriptedPlayoutBuilder.cs

@ -0,0 +1,76 @@ @@ -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);
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/IYamlPlayoutHandler.cs

@ -12,6 +12,6 @@ public interface IYamlPlayoutHandler @@ -12,6 +12,6 @@ public interface IYamlPlayoutHandler
YamlPlayoutInstruction instruction,
PlayoutBuildMode mode,
Func<string, Task> executeSequence,
ILogger<YamlPlayoutBuilder> logger,
ILogger<SequentialPlayoutBuilder> logger,
CancellationToken cancellationToken);
}

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutAllHandler.cs

@ -14,7 +14,7 @@ public class YamlPlayoutAllHandler(EnumeratorCache enumeratorCache) : YamlPlayou @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutApplyHistoryHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutApplyHistoryHandler(EnumeratorCache enumeratorCache) @@ -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))

6
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutContentHandler.cs

@ -17,13 +17,13 @@ public abstract class YamlPlayoutContentHandler(EnumeratorCache enumeratorCache) @@ -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) @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutCountHandler.cs

@ -15,7 +15,7 @@ public class YamlPlayoutCountHandler(EnumeratorCache enumeratorCache) : YamlPlay @@ -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)

4
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutDurationHandler.cs

@ -15,7 +15,7 @@ public class YamlPlayoutDurationHandler(EnumeratorCache enumeratorCache) : YamlP @@ -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 @@ -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;

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutEpgGroupHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutEpgGroupHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOffHandler.cs

@ -16,7 +16,7 @@ public class YamlPlayoutGraphicsOffHandler(IGraphicsElementRepository graphicsEl @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutGraphicsOnHandler.cs

@ -22,7 +22,7 @@ public class YamlPlayoutGraphicsOnHandler(IGraphicsElementRepository graphicsEle @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutMidRollHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutMidRollHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadToNextHandler.cs

@ -11,7 +11,7 @@ public class YamlPlayoutPadToNextHandler(EnumeratorCache enumeratorCache) : Yaml @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPadUntilHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutPadUntilHandler(EnumeratorCache enumeratorCache) : YamlP @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPostRollHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutPostRollHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutPreRollHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutPreRollHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs

@ -14,7 +14,7 @@ public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRewindHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutRewindHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutShuffleSequenceHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutShuffleSequenceHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipItemsHandler.cs

@ -14,7 +14,7 @@ public class YamlPlayoutSkipItemsHandler(EnumeratorCache enumeratorCache) : IYam @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutSkipToItemHandler.cs

@ -14,7 +14,7 @@ public class YamlPlayoutSkipToItemHandler(EnumeratorCache enumeratorCache) : IYa @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWaitUntilHandler.cs

@ -12,7 +12,7 @@ public class YamlPlayoutWaitUntilHandler : IYamlPlayoutHandler @@ -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)

2
ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutWatermarkHandler.cs

@ -16,7 +16,7 @@ public class YamlPlayoutWatermarkHandler(IChannelRepository channelRepository) : @@ -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)

16
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs → ErsatzTV.Core/Scheduling/YamlScheduling/SequentialPlayoutBuilder.cs

@ -14,15 +14,15 @@ using YamlDotNet.Serialization.NamingConventions; @@ -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( @@ -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( @@ -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))
{

6334
ErsatzTV.Infrastructure.MySql/Migrations/20250823155914_Add_PlayoutScheduleFile.Designer.cs generated

File diff suppressed because it is too large Load Diff

31
ErsatzTV.Infrastructure.MySql/Migrations/20250823155914_Add_PlayoutScheduleFile.cs

@ -0,0 +1,31 @@ @@ -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
ErsatzTV.Infrastructure.MySql/Migrations/20250823160902_Remove_PlayoutExternalJsonFileTemplateFile.Designer.cs generated

File diff suppressed because it is too large Load Diff

40
ErsatzTV.Infrastructure.MySql/Migrations/20250823160902_Remove_PlayoutExternalJsonFileTemplateFile.cs

@ -0,0 +1,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");
}
}
}

9
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -1822,24 +1822,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -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");

6169
ErsatzTV.Infrastructure.Sqlite/Migrations/20250823155845_Add_PlayoutScheduleFile.Designer.cs generated

File diff suppressed because it is too large Load Diff

30
ErsatzTV.Infrastructure.Sqlite/Migrations/20250823155845_Add_PlayoutScheduleFile.cs

@ -0,0 +1,30 @@ @@ -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
ErsatzTV.Infrastructure.Sqlite/Migrations/20250823160940_Remove_PlayoutExternalJsonFileTemplateFile.Designer.cs generated

File diff suppressed because it is too large Load Diff

38
ErsatzTV.Infrastructure.Sqlite/Migrations/20250823160940_Remove_PlayoutExternalJsonFileTemplateFile.cs

@ -0,0 +1,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);
}
}
}

9
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -1733,24 +1733,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -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");

8
ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs

@ -61,14 +61,14 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider @@ -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 @@ -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)

2
ErsatzTV/Controllers/Api/ChannelController.cs

@ -30,7 +30,7 @@ public class ChannelController(ChannelWriter<IBackgroundServiceRequest> workerCh @@ -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)
// {

12
ErsatzTV/Pages/PlayoutEditor.razor

@ -41,7 +41,7 @@ @@ -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 @@ @@ -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:

31
ErsatzTV/Pages/Playouts.razor

@ -25,6 +25,7 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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();

6
ErsatzTV/Pages/SequentialPlayoutEditor.razor

@ -1,4 +1,4 @@ @@ -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 @@ @@ -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 @@ @@ -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); },

9
ErsatzTV/Pages/Settings/XMLTVSettings.razor

@ -28,6 +28,15 @@ @@ -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>

1
ErsatzTV/PlayoutKind.cs

@ -4,5 +4,6 @@ public static class PlayoutKind @@ -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";
}

4
ErsatzTV/Startup.cs

@ -37,6 +37,7 @@ using ErsatzTV.Core.Metadata; @@ -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 @@ -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>();

3
ErsatzTV/Validators/PlayoutEditViewModelValidator.cs

@ -10,7 +10,8 @@ public class PlayoutEditViewModelValidator : AbstractValidator<PlayoutEditViewMo @@ -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) =>

8
ErsatzTV/ViewModels/PlayoutEditViewModel.cs

@ -9,14 +9,14 @@ public class PlayoutEditViewModel @@ -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)
};

Loading…
Cancel
Save