Browse Source

rename yaml playout to sequential schedule (#2335)

* clarify some schedule and playout terms

* more renaming
pull/2339/head
Jason Dove 11 months ago committed by GitHub
parent
commit
e06ee54070
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs
  3. 2
      ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs
  4. 24
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  5. 14
      ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs
  6. 32
      ErsatzTV.Application/Playouts/Commands/CreateClassicPlayoutHandler.cs
  7. 14
      ErsatzTV.Application/Playouts/Commands/CreateExternalJsonPlayoutHandler.cs
  8. 14
      ErsatzTV.Application/Playouts/Commands/CreatePlayout.cs
  9. 34
      ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs
  10. 12
      ErsatzTV.Application/Playouts/Commands/ResetAllPlayoutsHandler.cs
  11. 2
      ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs
  12. 2
      ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs
  13. 2
      ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayout.cs
  14. 14
      ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs
  15. 2
      ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs
  16. 2
      ErsatzTV.Application/Playouts/PlayoutViewModel.cs
  17. 2
      ErsatzTV.Application/Playouts/Queries/GetAllPlayoutsHandler.cs
  18. 2
      ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs
  19. 4
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutHistoryHandler.cs
  20. 4
      ErsatzTV.Application/Scheduling/Commands/ErasePlayoutItemsHandler.cs
  21. 2
      ErsatzTV.Application/Scheduling/Commands/PreviewBlockPlayoutHandler.cs
  22. 12
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  23. 2
      ErsatzTV.Core/Domain/Playout.cs
  24. 4
      ErsatzTV.Core/Domain/PlayoutScheduleKind.cs
  25. 2
      ErsatzTV.Core/Interfaces/Scheduling/ISequentialScheduleValidator.cs
  26. 4
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  27. 2
      ErsatzTV.Core/Scheduling/YamlScheduling/Handlers/YamlPlayoutRepeatHandler.cs
  28. 4
      ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs
  29. 6331
      ErsatzTV.Infrastructure.MySql/Migrations/20250823133839_Rename_ProgramSchedulePlayoutType_ScheduleKind.Designer.cs
  30. 28
      ErsatzTV.Infrastructure.MySql/Migrations/20250823133839_Rename_ProgramSchedulePlayoutType_ScheduleKind.cs
  31. 2
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  32. 6166
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250823133952_Rename_ProgramSchedulePlayoutType_ScheduleKind.Designer.cs
  33. 28
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250823133952_Rename_ProgramSchedulePlayoutType_ScheduleKind.cs
  34. 2
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  35. 10
      ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs
  36. 2
      ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs
  37. 4
      ErsatzTV/ErsatzTV.csproj
  38. 32
      ErsatzTV/Pages/PlayoutEditor.razor
  39. 65
      ErsatzTV/Pages/Playouts.razor
  40. 16
      ErsatzTV/Pages/SequentialPlayoutEditor.razor
  41. 6
      ErsatzTV/Pages/YamlValidator.razor
  42. 2
      ErsatzTV/PlayoutKind.cs
  43. 6
      ErsatzTV/Resources/sequential-schedule-import.schema.json
  44. 6
      ErsatzTV/Resources/sequential-schedule.schema.json
  45. 4
      ErsatzTV/Services/RunOnce/ResourceExtractorService.cs
  46. 2
      ErsatzTV/Startup.cs
  47. 6
      ErsatzTV/ViewModels/PlayoutEditViewModel.cs

8
CHANGELOG.md

@ -78,6 +78,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix error when changing default (lowest priority) alternate schedule - Fix error when changing default (lowest priority) alternate schedule
### Changed ### Changed
- Rename some schedule and playout terms for clarity
- Schedules are used to build playouts and are what actually differs
- The playout is the end result, and is the same no matter what schedule kind is used
- Supported schedule kinds:
- `Classic Schedules`
- `Block Schedules`
- `Sequential Schedules` (formerly `YAML Schedules` or `YAML Playouts`)
- `JSON (dizqueTV) Schedules` (formerly `External JSON Playouts`)
- Allow multiple watermarks in playback troubleshooting - Allow multiple watermarks in playback troubleshooting
- Classic schedules: allow selecting multiple watermarks on schedule items - Classic schedules: allow selecting multiple watermarks on schedule items
- Block schedules: allow selecting multiple watermarks on decos - Block schedules: allow selecting multiple watermarks on decos

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

@ -190,10 +190,10 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
foreach (Playout playout in playouts) foreach (Playout playout in playouts)
{ {
switch (playout.ProgramSchedulePlayoutType) switch (playout.ScheduleKind)
{ {
case ProgramSchedulePlayoutType.Classic: case PlayoutScheduleKind.Classic:
case ProgramSchedulePlayoutType.Yaml: case PlayoutScheduleKind.Sequential:
var floodSorted = playouts var floodSorted = playouts
.Collect(p => p.Items) .Collect(p => p.Items)
.OrderBy(pi => pi.Start) .OrderBy(pi => pi.Start)
@ -211,7 +211,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
minifier, minifier,
xml); xml);
break; break;
case ProgramSchedulePlayoutType.Block: case PlayoutScheduleKind.Block:
var blockSorted = playouts var blockSorted = playouts
.Collect(p => p.Items) .Collect(p => p.Items)
.OrderBy(pi => pi.Start) .OrderBy(pi => pi.Start)
@ -229,7 +229,7 @@ public class RefreshChannelDataHandler : IRequestHandler<RefreshChannelData>
minifier, minifier,
xml); xml);
break; break;
case ProgramSchedulePlayoutType.ExternalJson: case PlayoutScheduleKind.ExternalJson:
var externalJsonSorted = (await CollectExternalJsonItems(playout.ExternalJsonFile)) var externalJsonSorted = (await CollectExternalJsonItems(playout.ExternalJsonFile))
.Filter(pi => pi.StartOffset <= finish) .Filter(pi => pi.StartOffset <= finish)
.ToList(); .ToList();

2
ErsatzTV.Application/MediaCollections/Commands/PreviewPlaylistPlayoutHandler.cs

@ -32,7 +32,7 @@ public class PreviewPlaylistPlayoutHandler(
Name = "Playlist Preview" Name = "Playlist Preview"
}, },
Items = [], Items = [],
ProgramSchedulePlayoutType = ProgramSchedulePlayoutType.Classic, ScheduleKind = PlayoutScheduleKind.Classic,
PlayoutHistory = [], PlayoutHistory = [],
ProgramSchedule = new ProgramSchedule ProgramSchedule = new ProgramSchedule
{ {

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

@ -108,14 +108,14 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
PlayoutReferenceData referenceData = await GetReferenceData( PlayoutReferenceData referenceData = await GetReferenceData(
dbContext, dbContext,
playout.Id, playout.Id,
playout.ProgramSchedulePlayoutType); playout.ScheduleKind);
string channelNumber = referenceData.Channel.Number; string channelNumber = referenceData.Channel.Number;
channelName = referenceData.Channel.Name; channelName = referenceData.Channel.Name;
PlayoutBuildResult result = PlayoutBuildResult.Empty; PlayoutBuildResult result = PlayoutBuildResult.Empty;
switch (playout.ProgramSchedulePlayoutType) switch (playout.ScheduleKind)
{ {
case ProgramSchedulePlayoutType.Block: case PlayoutScheduleKind.Block:
result = await _blockPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken); result = await _blockPlayoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
result = await _blockPlayoutFillerBuilder.Build( result = await _blockPlayoutFillerBuilder.Build(
playout, playout,
@ -124,7 +124,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
request.Mode, request.Mode,
cancellationToken); cancellationToken);
break; break;
case ProgramSchedulePlayoutType.Yaml: case PlayoutScheduleKind.Sequential:
result = await _yamlPlayoutBuilder.Build( result = await _yamlPlayoutBuilder.Build(
request.Start, request.Start,
playout, playout,
@ -132,11 +132,11 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
request.Mode, request.Mode,
cancellationToken); cancellationToken);
break; break;
case ProgramSchedulePlayoutType.ExternalJson: case PlayoutScheduleKind.ExternalJson:
await _externalJsonPlayoutBuilder.Build(playout, request.Mode, cancellationToken); await _externalJsonPlayoutBuilder.Build(playout, request.Mode, cancellationToken);
break; break;
case ProgramSchedulePlayoutType.None: case PlayoutScheduleKind.None:
case ProgramSchedulePlayoutType.Classic: case PlayoutScheduleKind.Classic:
default: default:
result = await _playoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken); result = await _playoutBuilder.Build(playout, referenceData, request.Mode, cancellationToken);
break; break;
@ -223,7 +223,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml"); string fileName = Path.Combine(FileSystemLayout.ChannelGuideCacheFolder, $"{channelNumber}.xml");
if (hasChanges || !File.Exists(fileName) || if (hasChanges || !File.Exists(fileName) ||
playout.ProgramSchedulePlayoutType is ProgramSchedulePlayoutType.ExternalJson) playout.ScheduleKind is PlayoutScheduleKind.ExternalJson)
{ {
await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber), cancellationToken); await _workerChannel.WriteAsync(new RefreshChannelData(channelNumber), cancellationToken);
} }
@ -276,9 +276,9 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)
{ {
switch (playout.ProgramSchedulePlayoutType) switch (playout.ScheduleKind)
{ {
case ProgramSchedulePlayoutType.Classic: case PlayoutScheduleKind.Classic:
await dbContext.Entry(playout) await dbContext.Entry(playout)
.Collection(p => p.FillGroupIndices) .Collection(p => p.FillGroupIndices)
.LoadAsync(); .LoadAsync();
@ -311,7 +311,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
private static async Task<PlayoutReferenceData> GetReferenceData( private static async Task<PlayoutReferenceData> GetReferenceData(
TvContext dbContext, TvContext dbContext,
int playoutId, int playoutId,
ProgramSchedulePlayoutType playoutType) PlayoutScheduleKind scheduleKind)
{ {
Channel channel = await dbContext.Channels Channel channel = await dbContext.Channels
.AsNoTracking() .AsNoTracking()
@ -322,7 +322,7 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
List<PlayoutItem> existingItems = []; List<PlayoutItem> existingItems = [];
List<PlayoutTemplate> playoutTemplates = []; List<PlayoutTemplate> playoutTemplates = [];
if (playoutType is ProgramSchedulePlayoutType.Block) if (scheduleKind is PlayoutScheduleKind.Block)
{ {
deco = await dbContext.Decos deco = await dbContext.Decos
.AsNoTracking() .AsNoTracking()

14
ErsatzTV.Application/Playouts/Commands/CreateBlockPlayoutHandler.cs

@ -41,11 +41,11 @@ public class CreateBlockPlayoutHandler(
private static async Task<Validation<BaseError, Playout>> Validate( private static async Task<Validation<BaseError, Playout>> Validate(
TvContext dbContext, TvContext dbContext,
CreateBlockPlayout request) => CreateBlockPlayout request) =>
(await ValidateChannel(dbContext, request), ValidatePlayoutType(request)) (await ValidateChannel(dbContext, request), ValidateScheduleKind(request))
.Apply((channel, playoutType) => new Playout .Apply((channel, scheduleKind) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
ProgramSchedulePlayoutType = playoutType, ScheduleKind = scheduleKind,
Seed = new Random().Next() Seed = new Random().Next()
}); });
@ -64,9 +64,9 @@ public class CreateBlockPlayoutHandler(
.Map(_ => channel) .Map(_ => channel)
.ToValidation<BaseError>("Channel already has one playout"); .ToValidation<BaseError>("Channel already has one playout");
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType( private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind(
CreateBlockPlayout createBlockPlayout) => CreateBlockPlayout createBlockPlayout) =>
Optional(createBlockPlayout.ProgramSchedulePlayoutType) Optional(createBlockPlayout.ScheduleKind)
.Filter(playoutType => playoutType == ProgramSchedulePlayoutType.Block) .Filter(scheduleKind => scheduleKind == PlayoutScheduleKind.Block)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must be Block"); .ToValidation<BaseError>("[ScheduleKind] must be Block");
} }

32
ErsatzTV.Application/Playouts/Commands/CreateFloodPlayoutHandler.cs → ErsatzTV.Application/Playouts/Commands/CreateClassicPlayoutHandler.cs

@ -10,12 +10,12 @@ using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Either<BaseError, CreatePlayoutResponse>> public class CreateClassicPlayoutHandler : IRequestHandler<CreateClassicPlayout, Either<BaseError, CreatePlayoutResponse>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
public CreateFloodPlayoutHandler( public CreateClassicPlayoutHandler(
ChannelWriter<IBackgroundServiceRequest> channel, ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory) IDbContextFactory<TvContext> dbContextFactory)
{ {
@ -24,7 +24,7 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
} }
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle( public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
CreateFloodPlayout request, CreateClassicPlayout request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
@ -48,22 +48,22 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
private static async Task<Validation<BaseError, Playout>> Validate( private static async Task<Validation<BaseError, Playout>> Validate(
TvContext dbContext, TvContext dbContext,
CreateFloodPlayout request) => CreateClassicPlayout request) =>
(await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request), (await ValidateChannel(dbContext, request), await ValidateProgramSchedule(dbContext, request),
ValidatePlayoutType(request)) ValidateScheduleKind(request))
.Apply((channel, programSchedule, playoutType) => new Playout .Apply((channel, programSchedule, scheduleKind) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
ProgramScheduleId = programSchedule.Id, ProgramScheduleId = programSchedule.Id,
ProgramSchedulePlayoutType = playoutType ScheduleKind = scheduleKind
}); });
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext, TvContext dbContext,
CreateFloodPlayout createFloodPlayout) => CreateClassicPlayout createClassicPlayout) =>
dbContext.Channels dbContext.Channels
.Include(c => c.Playouts) .Include(c => c.Playouts)
.SelectOneAsync(c => c.Id, c => c.Id == createFloodPlayout.ChannelId) .SelectOneAsync(c => c.Id, c => c.Id == createClassicPlayout.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist")) .Map(o => o.ToValidation<BaseError>("Channel does not exist"))
.BindT(ChannelMustNotHavePlayouts); .BindT(ChannelMustNotHavePlayouts);
@ -75,10 +75,10 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
private static Task<Validation<BaseError, ProgramSchedule>> ValidateProgramSchedule( private static Task<Validation<BaseError, ProgramSchedule>> ValidateProgramSchedule(
TvContext dbContext, TvContext dbContext,
CreateFloodPlayout createFloodPlayout) => CreateClassicPlayout createClassicPlayout) =>
dbContext.ProgramSchedules dbContext.ProgramSchedules
.Include(ps => ps.Items) .Include(ps => ps.Items)
.SelectOneAsync(ps => ps.Id, ps => ps.Id == createFloodPlayout.ProgramScheduleId) .SelectOneAsync(ps => ps.Id, ps => ps.Id == createClassicPlayout.ProgramScheduleId)
.Map(o => o.ToValidation<BaseError>("Program schedule does not exist")) .Map(o => o.ToValidation<BaseError>("Program schedule does not exist"))
.BindT(ProgramScheduleMustHaveItems); .BindT(ProgramScheduleMustHaveItems);
@ -88,9 +88,9 @@ public class CreateFloodPlayoutHandler : IRequestHandler<CreateFloodPlayout, Eit
.Filter(ps => ps.Items.Count != 0) .Filter(ps => ps.Items.Count != 0)
.ToValidation<BaseError>("Program schedule must have items"); .ToValidation<BaseError>("Program schedule must have items");
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType( private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind(
CreateFloodPlayout createFloodPlayout) => CreateClassicPlayout createClassicPlayout) =>
Optional(createFloodPlayout.ProgramSchedulePlayoutType) Optional(createClassicPlayout.ScheduleKind)
.Filter(playoutType => playoutType != ProgramSchedulePlayoutType.None) .Filter(scheduleKind => scheduleKind == PlayoutScheduleKind.Classic)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must not be None"); .ToValidation<BaseError>("[ScheduleKind] must be Classic");
} }

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

@ -49,12 +49,12 @@ public class CreateExternalJsonPlayoutHandler
private async Task<Validation<BaseError, Playout>> Validate( private async Task<Validation<BaseError, Playout>> Validate(
TvContext dbContext, TvContext dbContext,
CreateExternalJsonPlayout request) => CreateExternalJsonPlayout request) =>
(await ValidateChannel(dbContext, request), ValidateExternalJsonFile(request), ValidatePlayoutType(request)) (await ValidateChannel(dbContext, request), ValidateExternalJsonFile(request), ValidateScheduleKind(request))
.Apply((channel, externalJsonFile, playoutType) => new Playout .Apply((channel, externalJsonFile, scheduleKind) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
ExternalJsonFile = externalJsonFile, ExternalJsonFile = externalJsonFile,
ProgramSchedulePlayoutType = playoutType ScheduleKind = scheduleKind
}); });
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(
@ -82,9 +82,9 @@ public class CreateExternalJsonPlayoutHandler
return request.ExternalJsonFile; return request.ExternalJsonFile;
} }
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType( private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind(
CreateExternalJsonPlayout createExternalJsonPlayout) => CreateExternalJsonPlayout createExternalJsonPlayout) =>
Optional(createExternalJsonPlayout.ProgramSchedulePlayoutType) Optional(createExternalJsonPlayout.ScheduleKind)
.Filter(playoutType => playoutType == ProgramSchedulePlayoutType.ExternalJson) .Filter(scheduleKind => scheduleKind == PlayoutScheduleKind.ExternalJson)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must be ExternalJson"); .ToValidation<BaseError>("[ScheduleKind] must be ExternalJson");
} }

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

@ -3,17 +3,17 @@ using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
public record CreatePlayout(int ChannelId, ProgramSchedulePlayoutType ProgramSchedulePlayoutType) public record CreatePlayout(int ChannelId, PlayoutScheduleKind ScheduleKind)
: IRequest<Either<BaseError, CreatePlayoutResponse>>; : IRequest<Either<BaseError, CreatePlayoutResponse>>;
public record CreateFloodPlayout(int ChannelId, int ProgramScheduleId) public record CreateClassicPlayout(int ChannelId, int ProgramScheduleId)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Classic); : CreatePlayout(ChannelId, PlayoutScheduleKind.Classic);
public record CreateBlockPlayout(int ChannelId) public record CreateBlockPlayout(int ChannelId)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Block); : CreatePlayout(ChannelId, PlayoutScheduleKind.Block);
public record CreateYamlPlayout(int ChannelId, string TemplateFile) public record CreateSequentialPlayout(int ChannelId, string TemplateFile)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.Yaml); : CreatePlayout(ChannelId, PlayoutScheduleKind.Sequential);
public record CreateExternalJsonPlayout(int ChannelId, string ExternalJsonFile) public record CreateExternalJsonPlayout(int ChannelId, string ExternalJsonFile)
: CreatePlayout(ChannelId, ProgramSchedulePlayoutType.ExternalJson); : CreatePlayout(ChannelId, PlayoutScheduleKind.ExternalJson);

34
ErsatzTV.Application/Playouts/Commands/CreateYamlPlayoutHandler.cs → ErsatzTV.Application/Playouts/Commands/CreateSequentialPlayoutHandler.cs

@ -11,14 +11,14 @@ using Channel = ErsatzTV.Core.Domain.Channel;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
public class CreateYamlPlayoutHandler public class CreateSequentialPlayoutHandler
: IRequestHandler<CreateYamlPlayout, Either<BaseError, CreatePlayoutResponse>> : IRequestHandler<CreateSequentialPlayout, Either<BaseError, CreatePlayoutResponse>>
{ {
private readonly ChannelWriter<IBackgroundServiceRequest> _channel; private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ILocalFileSystem _localFileSystem; private readonly ILocalFileSystem _localFileSystem;
public CreateYamlPlayoutHandler( public CreateSequentialPlayoutHandler(
ILocalFileSystem localFileSystem, ILocalFileSystem localFileSystem,
ChannelWriter<IBackgroundServiceRequest> channel, ChannelWriter<IBackgroundServiceRequest> channel,
IDbContextFactory<TvContext> dbContextFactory) IDbContextFactory<TvContext> dbContextFactory)
@ -29,7 +29,7 @@ public class CreateYamlPlayoutHandler
} }
public async Task<Either<BaseError, CreatePlayoutResponse>> Handle( public async Task<Either<BaseError, CreatePlayoutResponse>> Handle(
CreateYamlPlayout request, CreateSequentialPlayout request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
@ -53,22 +53,22 @@ public class CreateYamlPlayoutHandler
private async Task<Validation<BaseError, Playout>> Validate( private async Task<Validation<BaseError, Playout>> Validate(
TvContext dbContext, TvContext dbContext,
CreateYamlPlayout request) => CreateSequentialPlayout request) =>
(await ValidateChannel(dbContext, request), ValidateYamlFile(request), ValidatePlayoutType(request)) (await ValidateChannel(dbContext, request), ValidateYamlFile(request), ValidateScheduleKind(request))
.Apply((channel, externalJsonFile, playoutType) => new Playout .Apply((channel, yamlFile, scheduleKind) => new Playout
{ {
ChannelId = channel.Id, ChannelId = channel.Id,
TemplateFile = externalJsonFile, TemplateFile = yamlFile,
ProgramSchedulePlayoutType = playoutType, ScheduleKind = scheduleKind,
Seed = new Random().Next() Seed = new Random().Next()
}); });
private static Task<Validation<BaseError, Channel>> ValidateChannel( private static Task<Validation<BaseError, Channel>> ValidateChannel(
TvContext dbContext, TvContext dbContext,
CreateYamlPlayout createYamlPlayout) => CreateSequentialPlayout createSequentialPlayout) =>
dbContext.Channels dbContext.Channels
.Include(c => c.Playouts) .Include(c => c.Playouts)
.SelectOneAsync(c => c.Id, c => c.Id == createYamlPlayout.ChannelId) .SelectOneAsync(c => c.Id, c => c.Id == createSequentialPlayout.ChannelId)
.Map(o => o.ToValidation<BaseError>("Channel does not exist")) .Map(o => o.ToValidation<BaseError>("Channel does not exist"))
.BindT(ChannelMustNotHavePlayouts); .BindT(ChannelMustNotHavePlayouts);
@ -78,7 +78,7 @@ public class CreateYamlPlayoutHandler
.Map(_ => channel) .Map(_ => channel)
.ToValidation<BaseError>("Channel already has one playout"); .ToValidation<BaseError>("Channel already has one playout");
private Validation<BaseError, string> ValidateYamlFile(CreateYamlPlayout request) private Validation<BaseError, string> ValidateYamlFile(CreateSequentialPlayout request)
{ {
if (!_localFileSystem.FileExists(request.TemplateFile)) if (!_localFileSystem.FileExists(request.TemplateFile))
{ {
@ -88,9 +88,9 @@ public class CreateYamlPlayoutHandler
return request.TemplateFile; return request.TemplateFile;
} }
private static Validation<BaseError, ProgramSchedulePlayoutType> ValidatePlayoutType( private static Validation<BaseError, PlayoutScheduleKind> ValidateScheduleKind(
CreateYamlPlayout createYamlPlayout) => CreateSequentialPlayout createSequentialPlayout) =>
Optional(createYamlPlayout.ProgramSchedulePlayoutType) Optional(createSequentialPlayout.ScheduleKind)
.Filter(playoutType => playoutType == ProgramSchedulePlayoutType.Yaml) .Filter(scheduleKind => scheduleKind == PlayoutScheduleKind.Sequential)
.ToValidation<BaseError>("[ProgramSchedulePlayoutType] must be YAML"); .ToValidation<BaseError>("[ScheduleKind] must be YAML");
} }

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

@ -19,11 +19,11 @@ public class ResetAllPlayoutsHandler(
foreach (Playout playout in await dbContext.Playouts.ToListAsync(cancellationToken)) foreach (Playout playout in await dbContext.Playouts.ToListAsync(cancellationToken))
{ {
switch (playout.ProgramSchedulePlayoutType) switch (playout.ScheduleKind)
{ {
case ProgramSchedulePlayoutType.Classic: case PlayoutScheduleKind.Classic:
case ProgramSchedulePlayoutType.Block: case PlayoutScheduleKind.Block:
case ProgramSchedulePlayoutType.Yaml: case PlayoutScheduleKind.Sequential:
if (!locker.IsPlayoutLocked(playout.Id)) if (!locker.IsPlayoutLocked(playout.Id))
{ {
await channel.WriteAsync( await channel.WriteAsync(
@ -32,8 +32,8 @@ public class ResetAllPlayoutsHandler(
} }
break; break;
case ProgramSchedulePlayoutType.ExternalJson: case PlayoutScheduleKind.ExternalJson:
case ProgramSchedulePlayoutType.None: case PlayoutScheduleKind.None:
default: default:
// external json cannot be reset // external json cannot be reset
continue; continue;

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

@ -46,7 +46,7 @@ public class
return new PlayoutNameViewModel( return new PlayoutNameViewModel(
playout.Id, playout.Id,
playout.ProgramSchedulePlayoutType, playout.ScheduleKind,
playout.Channel.Name, playout.Channel.Name,
playout.Channel.Number, playout.Channel.Number,
playout.Channel.PlayoutMode, playout.Channel.PlayoutMode,

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

@ -38,7 +38,7 @@ public class UpdatePlayoutHandler : IRequestHandler<UpdatePlayout, Either<BaseEr
return new PlayoutNameViewModel( return new PlayoutNameViewModel(
playout.Id, playout.Id,
playout.ProgramSchedulePlayoutType, playout.ScheduleKind,
playout.Channel.Name, playout.Channel.Name,
playout.Channel.Number, playout.Channel.Number,
playout.Channel.PlayoutMode, playout.Channel.PlayoutMode,

2
ErsatzTV.Application/Playouts/Commands/UpdateYamlPlayout.cs → ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayout.cs

@ -2,5 +2,5 @@
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
public record UpdateYamlPlayout(int PlayoutId, string TemplateFile) public record UpdateSequentialPlayout(int PlayoutId, string TemplateFile)
: IRequest<Either<BaseError, PlayoutNameViewModel>>; : IRequest<Either<BaseError, PlayoutNameViewModel>>;

14
ErsatzTV.Application/Playouts/Commands/UpdateTemplatePlayoutHandler.cs → ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs

@ -9,13 +9,13 @@ using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.Playouts; namespace ErsatzTV.Application.Playouts;
public class public class
UpdateTemplatePlayoutHandler : IRequestHandler<UpdateYamlPlayout, UpdateSequentialPlayoutHandler : IRequestHandler<UpdateSequentialPlayout,
Either<BaseError, PlayoutNameViewModel>> Either<BaseError, PlayoutNameViewModel>>
{ {
private readonly IDbContextFactory<TvContext> _dbContextFactory; private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel; private readonly ChannelWriter<IBackgroundServiceRequest> _workerChannel;
public UpdateTemplatePlayoutHandler( public UpdateSequentialPlayoutHandler(
IDbContextFactory<TvContext> dbContextFactory, IDbContextFactory<TvContext> dbContextFactory,
ChannelWriter<IBackgroundServiceRequest> workerChannel) ChannelWriter<IBackgroundServiceRequest> workerChannel)
{ {
@ -24,7 +24,7 @@ public class
} }
public async Task<Either<BaseError, PlayoutNameViewModel>> Handle( public async Task<Either<BaseError, PlayoutNameViewModel>> Handle(
UpdateYamlPlayout request, UpdateSequentialPlayout request,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
@ -34,7 +34,7 @@ public class
private async Task<PlayoutNameViewModel> ApplyUpdateRequest( private async Task<PlayoutNameViewModel> ApplyUpdateRequest(
TvContext dbContext, TvContext dbContext,
UpdateYamlPlayout request, UpdateSequentialPlayout request,
Playout playout) Playout playout)
{ {
playout.TemplateFile = request.TemplateFile; playout.TemplateFile = request.TemplateFile;
@ -46,7 +46,7 @@ public class
return new PlayoutNameViewModel( return new PlayoutNameViewModel(
playout.Id, playout.Id,
playout.ProgramSchedulePlayoutType, playout.ScheduleKind,
playout.Channel.Name, playout.Channel.Name,
playout.Channel.Number, playout.Channel.Number,
playout.Channel.PlayoutMode, playout.Channel.PlayoutMode,
@ -58,12 +58,12 @@ public class
private static Task<Validation<BaseError, Playout>> Validate( private static Task<Validation<BaseError, Playout>> Validate(
TvContext dbContext, TvContext dbContext,
UpdateYamlPlayout request) => UpdateSequentialPlayout request) =>
PlayoutMustExist(dbContext, request); PlayoutMustExist(dbContext, request);
private static Task<Validation<BaseError, Playout>> PlayoutMustExist( private static Task<Validation<BaseError, Playout>> PlayoutMustExist(
TvContext dbContext, TvContext dbContext,
UpdateYamlPlayout updatePlayout) => UpdateSequentialPlayout updatePlayout) =>
dbContext.Playouts dbContext.Playouts
.Include(p => p.Channel) .Include(p => p.Channel)
.SelectOneAsync(p => p.Id, p => p.Id == updatePlayout.PlayoutId) .SelectOneAsync(p => p.Id, p => p.Id == updatePlayout.PlayoutId)

2
ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs

@ -4,7 +4,7 @@ namespace ErsatzTV.Application.Playouts;
public record PlayoutNameViewModel( public record PlayoutNameViewModel(
int PlayoutId, int PlayoutId,
ProgramSchedulePlayoutType PlayoutType, PlayoutScheduleKind ScheduleKind,
string ChannelName, string ChannelName,
string ChannelNumber, string ChannelNumber,
ChannelPlayoutMode PlayoutMode, ChannelPlayoutMode PlayoutMode,

2
ErsatzTV.Application/Playouts/PlayoutViewModel.cs

@ -6,4 +6,4 @@ public record PlayoutViewModel(
int Id, int Id,
PlayoutChannelViewModel Channel, PlayoutChannelViewModel Channel,
PlayoutProgramScheduleViewModel ProgramSchedule, PlayoutProgramScheduleViewModel ProgramSchedule,
ProgramSchedulePlayoutType ProgramSchedulePlayoutType); PlayoutScheduleKind ScheduleKind);

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

@ -21,7 +21,7 @@ public class GetAllPlayoutsHandler : IRequestHandler<GetAllPlayouts, List<Playou
.Filter(p => p.Channel != null) .Filter(p => p.Channel != null)
.Map(p => new PlayoutNameViewModel( .Map(p => new PlayoutNameViewModel(
p.Id, p.Id,
p.ProgramSchedulePlayoutType, p.ScheduleKind,
p.Channel.Name, p.Channel.Name,
p.Channel.Number, p.Channel.Number,
p.Channel.PlayoutMode, p.Channel.PlayoutMode,

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

@ -19,7 +19,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory<TvContext> dbContextFactory
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId) .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId)
.MapT(p => new PlayoutNameViewModel( .MapT(p => new PlayoutNameViewModel(
p.Id, p.Id,
p.ProgramSchedulePlayoutType, p.ScheduleKind,
p.Channel.Name, p.Channel.Name,
p.Channel.Number, p.Channel.Number,
p.Channel.PlayoutMode, p.Channel.PlayoutMode,

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

@ -13,8 +13,8 @@ public class ErasePlayoutHistoryHandler(IDbContextFactory<TvContext> dbContextFa
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken); await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<Playout> maybePlayout = await dbContext.Playouts Option<Playout> maybePlayout = await dbContext.Playouts
.Filter(p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block || .Filter(p => p.ScheduleKind == PlayoutScheduleKind.Block ||
p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml) p.ScheduleKind == PlayoutScheduleKind.Sequential)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)

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

@ -16,8 +16,8 @@ public class ErasePlayoutItemsHandler(IDbContextFactory<TvContext> dbContextFact
Option<Playout> maybePlayout = await dbContext.Playouts Option<Playout> maybePlayout = await dbContext.Playouts
.Include(p => p.Items) .Include(p => p.Items)
.Include(p => p.PlayoutHistory) .Include(p => p.PlayoutHistory)
.Filter(p => p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Block || .Filter(p => p.ScheduleKind == PlayoutScheduleKind.Block ||
p.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.Yaml) p.ScheduleKind == PlayoutScheduleKind.Sequential)
.SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId); .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId);
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)

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

@ -42,7 +42,7 @@ public class PreviewBlockPlayoutHandler(
Name = "Block Preview" Name = "Block Preview"
}, },
Items = [], Items = [],
ProgramSchedulePlayoutType = ProgramSchedulePlayoutType.Block, ScheduleKind = PlayoutScheduleKind.Block,
PlayoutHistory = [], PlayoutHistory = [],
Templates = Templates =
[ [

12
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -133,7 +133,7 @@ public class ScheduleIntegrationTests
PlayoutReferenceData referenceData = await GetReferenceData( PlayoutReferenceData referenceData = await GetReferenceData(
context, context,
PLAYOUT_ID, PLAYOUT_ID,
ProgramSchedulePlayoutType.Classic); PlayoutScheduleKind.Classic);
await builder.Build( await builder.Build(
playout, playout,
@ -157,7 +157,7 @@ public class ScheduleIntegrationTests
PlayoutReferenceData referenceData = await GetReferenceData( PlayoutReferenceData referenceData = await GetReferenceData(
context, context,
PLAYOUT_ID, PLAYOUT_ID,
ProgramSchedulePlayoutType.Classic); PlayoutScheduleKind.Classic);
await builder.Build( await builder.Build(
playout, playout,
@ -181,7 +181,7 @@ public class ScheduleIntegrationTests
PlayoutReferenceData referenceData = await GetReferenceData( PlayoutReferenceData referenceData = await GetReferenceData(
context, context,
PLAYOUT_ID, PLAYOUT_ID,
ProgramSchedulePlayoutType.Classic); PlayoutScheduleKind.Classic);
await builder.Build( await builder.Build(
playout, playout,
@ -329,7 +329,7 @@ public class ScheduleIntegrationTests
PlayoutReferenceData referenceData = await GetReferenceData( PlayoutReferenceData referenceData = await GetReferenceData(
context, context,
playoutId, playoutId,
ProgramSchedulePlayoutType.Classic); PlayoutScheduleKind.Classic);
await builder.Build( await builder.Build(
playout, playout,
@ -397,7 +397,7 @@ public class ScheduleIntegrationTests
private static async Task<PlayoutReferenceData> GetReferenceData( private static async Task<PlayoutReferenceData> GetReferenceData(
TvContext dbContext, TvContext dbContext,
int playoutId, int playoutId,
ProgramSchedulePlayoutType playoutType) PlayoutScheduleKind scheduleKind)
{ {
Channel channel = await dbContext.Channels Channel channel = await dbContext.Channels
.AsNoTracking() .AsNoTracking()
@ -407,7 +407,7 @@ public class ScheduleIntegrationTests
List<PlayoutItem> existingItems = []; List<PlayoutItem> existingItems = [];
List<PlayoutTemplate> playoutTemplates = []; List<PlayoutTemplate> playoutTemplates = [];
if (playoutType is ProgramSchedulePlayoutType.Block) if (scheduleKind is PlayoutScheduleKind.Block)
{ {
existingItems = await dbContext.PlayoutItems existingItems = await dbContext.PlayoutItems
.AsNoTracking() .AsNoTracking()

2
ErsatzTV.Core/Domain/Playout.cs

@ -12,7 +12,7 @@ public class Playout
public string ExternalJsonFile { get; set; } public string ExternalJsonFile { get; set; }
public string TemplateFile { get; set; } public string TemplateFile { get; set; }
public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; } public List<ProgramScheduleAlternate> ProgramScheduleAlternates { get; set; }
public ProgramSchedulePlayoutType ProgramSchedulePlayoutType { get; set; } public PlayoutScheduleKind ScheduleKind { get; set; }
public List<PlayoutItem> Items { get; set; } public List<PlayoutItem> Items { get; set; }
public PlayoutAnchor Anchor { get; set; } public PlayoutAnchor Anchor { get; set; }
public List<PlayoutProgramScheduleAnchor> ProgramScheduleAnchors { get; set; } public List<PlayoutProgramScheduleAnchor> ProgramScheduleAnchors { get; set; }

4
ErsatzTV.Core/Domain/ProgramSchedulePlayoutType.cs → ErsatzTV.Core/Domain/PlayoutScheduleKind.cs

@ -1,11 +1,11 @@
namespace ErsatzTV.Core.Domain; namespace ErsatzTV.Core.Domain;
public enum ProgramSchedulePlayoutType public enum PlayoutScheduleKind
{ {
None = 0, None = 0,
Classic = 1, Classic = 1,
Block = 2, Block = 2,
Yaml = 3, Sequential = 3,
ExternalJson = 20 ExternalJson = 20
} }

2
ErsatzTV.Core/Interfaces/Scheduling/IYamlScheduleValidator.cs → ErsatzTV.Core/Interfaces/Scheduling/ISequentialScheduleValidator.cs

@ -1,6 +1,6 @@
namespace ErsatzTV.Core.Interfaces.Scheduling; namespace ErsatzTV.Core.Interfaces.Scheduling;
public interface IYamlScheduleValidator public interface ISequentialScheduleValidator
{ {
Task<bool> ValidateSchedule(string yaml, bool isImport); Task<bool> ValidateSchedule(string yaml, bool isImport);
string ToJson(string yaml); string ToJson(string yaml);

4
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -67,11 +67,11 @@ public class PlayoutBuilder : IPlayoutBuilder
{ {
PlayoutBuildResult result = PlayoutBuildResult.Empty; PlayoutBuildResult result = PlayoutBuildResult.Empty;
if (playout.ProgramSchedulePlayoutType is not ProgramSchedulePlayoutType.Classic) if (playout.ScheduleKind is not PlayoutScheduleKind.Classic)
{ {
_logger.LogWarning( _logger.LogWarning(
"Skipping playout build with type {Type} on channel {Number} - {Name}", "Skipping playout build with type {Type} on channel {Number} - {Name}",
playout.ProgramSchedulePlayoutType, playout.ScheduleKind,
referenceData.Channel.Number, referenceData.Channel.Number,
referenceData.Channel.Name); referenceData.Channel.Name);

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

@ -25,7 +25,7 @@ public class YamlPlayoutRepeatHandler : IYamlPlayoutHandler
if (context.VisitedAll && _itemsSinceLastRepeat == context.AddedItems.Count) if (context.VisitedAll && _itemsSinceLastRepeat == context.AddedItems.Count)
{ {
logger.LogWarning("Repeat encountered without adding any playout items; aborting"); logger.LogWarning("Repeat encountered without adding any playout items; aborting");
throw new InvalidOperationException("YAML playout loop detected"); throw new InvalidOperationException("Sequential playout loop detected");
} }
_itemsSinceLastRepeat = context.AddedItems.Count; _itemsSinceLastRepeat = context.AddedItems.Count;

4
ErsatzTV.Core/Scheduling/YamlScheduling/YamlPlayoutBuilder.cs

@ -20,7 +20,7 @@ public class YamlPlayoutBuilder(
IMediaCollectionRepository mediaCollectionRepository, IMediaCollectionRepository mediaCollectionRepository,
IChannelRepository channelRepository, IChannelRepository channelRepository,
IGraphicsElementRepository graphicsElementRepository, IGraphicsElementRepository graphicsElementRepository,
IYamlScheduleValidator yamlScheduleValidator, ISequentialScheduleValidator sequentialScheduleValidator,
ILogger<YamlPlayoutBuilder> logger) ILogger<YamlPlayoutBuilder> logger)
: IYamlPlayoutBuilder : IYamlPlayoutBuilder
{ {
@ -455,7 +455,7 @@ public class YamlPlayoutBuilder(
try try
{ {
string yaml = await File.ReadAllTextAsync(fileName, cancellationToken); string yaml = await File.ReadAllTextAsync(fileName, cancellationToken);
if (!await yamlScheduleValidator.ValidateSchedule(yaml, isImport)) if (!await sequentialScheduleValidator.ValidateSchedule(yaml, isImport))
{ {
return Option<YamlPlayoutDefinition>.None; return Option<YamlPlayoutDefinition>.None;
} }

6331
ErsatzTV.Infrastructure.MySql/Migrations/20250823133839_Rename_ProgramSchedulePlayoutType_ScheduleKind.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.MySql/Migrations/20250823133839_Rename_ProgramSchedulePlayoutType_ScheduleKind.cs

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Rename_ProgramSchedulePlayoutType_ScheduleKind : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ProgramSchedulePlayoutType",
table: "Playout",
newName: "ScheduleKind");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ScheduleKind",
table: "Playout",
newName: "ProgramSchedulePlayoutType");
}
}
}

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

@ -1831,7 +1831,7 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
b.Property<int?>("ProgramScheduleId") b.Property<int?>("ProgramScheduleId")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("ProgramSchedulePlayoutType") b.Property<int>("ScheduleKind")
.HasColumnType("int"); .HasColumnType("int");
b.Property<int>("Seed") b.Property<int>("Seed")

6166
ErsatzTV.Infrastructure.Sqlite/Migrations/20250823133952_Rename_ProgramSchedulePlayoutType_ScheduleKind.Designer.cs generated

File diff suppressed because it is too large Load Diff

28
ErsatzTV.Infrastructure.Sqlite/Migrations/20250823133952_Rename_ProgramSchedulePlayoutType_ScheduleKind.cs

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Rename_ProgramSchedulePlayoutType_ScheduleKind : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ProgramSchedulePlayoutType",
table: "Playout",
newName: "ScheduleKind");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ScheduleKind",
table: "Playout",
newName: "ProgramSchedulePlayoutType");
}
}
}

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

@ -1742,7 +1742,7 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
b.Property<int?>("ProgramScheduleId") b.Property<int?>("ProgramScheduleId")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("ProgramSchedulePlayoutType") b.Property<int>("ScheduleKind")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<int>("Seed") b.Property<int>("Seed")

10
ErsatzTV.Infrastructure/Scheduling/YamlScheduleValidator.cs → ErsatzTV.Infrastructure/Scheduling/SequentialScheduleValidator.cs

@ -11,7 +11,7 @@ using JsonSerializer = System.Text.Json.JsonSerializer;
namespace ErsatzTV.Infrastructure.Scheduling; namespace ErsatzTV.Infrastructure.Scheduling;
public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYamlScheduleValidator public class SequentialScheduleValidator(ILogger<SequentialScheduleValidator> logger) : ISequentialScheduleValidator
{ {
public async Task<bool> ValidateSchedule(string yaml, bool isImport) public async Task<bool> ValidateSchedule(string yaml, bool isImport)
{ {
@ -19,7 +19,7 @@ public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYam
{ {
string schemaFileName = Path.Combine( string schemaFileName = Path.Combine(
FileSystemLayout.ResourcesCacheFolder, FileSystemLayout.ResourcesCacheFolder,
isImport ? "yaml-playout-import.schema.json" : "yaml-playout.schema.json"); isImport ? "sequential-schedule-import.schema.json" : "sequential-schedule.schema.json");
using StreamReader sr = File.OpenText(schemaFileName); using StreamReader sr = File.OpenText(schemaFileName);
await using var reader = new JsonTextReader(sr); await using var reader = new JsonTextReader(sr);
var schema = JSchema.Load(reader); var schema = JSchema.Load(reader);
@ -31,7 +31,7 @@ public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYam
if (!schedule.IsValid(schema, out IList<string> errorMessages)) if (!schedule.IsValid(schema, out IList<string> errorMessages))
{ {
logger.LogWarning("Failed to validate YAML schedule definition: {ErrorMessages}", errorMessages); logger.LogWarning("Failed to validate sequential schedule definition: {ErrorMessages}", errorMessages);
return false; return false;
} }
@ -39,7 +39,7 @@ public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYam
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogWarning(ex, "Unexpected error while validating YAML schedule definition"); logger.LogWarning(ex, "Unexpected error while validating sequential schedule definition");
} }
return false; return false;
@ -62,7 +62,7 @@ public class YamlScheduleValidator(ILogger<YamlScheduleValidator> logger) : IYam
{ {
string schemaFileName = Path.Combine( string schemaFileName = Path.Combine(
FileSystemLayout.ResourcesCacheFolder, FileSystemLayout.ResourcesCacheFolder,
isImport ? "yaml-playout-import.schema.json" : "yaml-playout.schema.json"); isImport ? "sequential-schedule-import.schema.json" : "sequential-schedule.schema.json");
using StreamReader sr = File.OpenText(schemaFileName); using StreamReader sr = File.OpenText(schemaFileName);
await using var reader = new JsonTextReader(sr); await using var reader = new JsonTextReader(sr);
var schema = JSchema.Load(reader); var schema = JSchema.Load(reader);

2
ErsatzTV.Infrastructure/Streaming/ExternalJsonPlayoutItemProvider.cs

@ -58,7 +58,7 @@ public class ExternalJsonPlayoutItemProvider : IExternalJsonPlayoutItemProvider
foreach (Playout playout in maybePlayout) foreach (Playout playout in maybePlayout)
{ {
// playout must be external json // playout must be external json
if (playout.ProgramSchedulePlayoutType == ProgramSchedulePlayoutType.ExternalJson) if (playout.ScheduleKind == PlayoutScheduleKind.ExternalJson)
{ {
// json file must exist // json file must exist
if (_localFileSystem.FileExists(playout.ExternalJsonFile)) if (_localFileSystem.FileExists(playout.ExternalJsonFile))

4
ErsatzTV/ErsatzTV.csproj

@ -81,8 +81,8 @@
<EmbeddedResource Include="Resources\Templates\_musicVideo.sbntxt" /> <EmbeddedResource Include="Resources\Templates\_musicVideo.sbntxt" />
<EmbeddedResource Include="Resources\Templates\_otherVideo.sbntxt" /> <EmbeddedResource Include="Resources\Templates\_otherVideo.sbntxt" />
<EmbeddedResource Include="Resources\Templates\_song.sbntxt" /> <EmbeddedResource Include="Resources\Templates\_song.sbntxt" />
<EmbeddedResource Include="Resources\yaml-playout-import.schema.json" /> <EmbeddedResource Include="Resources\sequential-schedule-import.schema.json" />
<EmbeddedResource Include="Resources\yaml-playout.schema.json" /> <EmbeddedResource Include="Resources\sequential-schedule.schema.json" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

32
ErsatzTV/Pages/PlayoutEditor.razor

@ -13,22 +13,8 @@
<MudForm @ref="_form" Validation="@(_validator.ValidateValue)" ValidationDelay="0" Style="max-height: 100%"> <MudForm @ref="_form" Validation="@(_validator.ValidateValue)" ValidationDelay="0" Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Class="ml-6" OnClick="HandleSubmitAsync" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"> <MudButton Class="ml-6" OnClick="@HandleSubmitAsync" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add">
@switch (Kind) Add Playout
{
case PlayoutKind.ExternalJson:
@:Add External Json Playout
break;
case PlayoutKind.Yaml:
@:Add YAML Playout
break;
case PlayoutKind.Block:
@:Add Block Playout
break;
default:
@:Add Playout
break;
}
</MudButton> </MudButton>
</MudPaper> </MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto"> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
@ -53,25 +39,29 @@
case PlayoutKind.ExternalJson: case PlayoutKind.ExternalJson:
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"> <div class="d-flex">
<MudText>External Json File</MudText> <MudText>JSON (dizqueTV) Schedule</MudText>
</div> </div>
<MudTextField @bind-Value="_model.ExternalJsonFile" For="@(() => _model.ExternalJsonFile)"/> <MudTextField @bind-Value="_model.ExternalJsonFile" For="@(() => _model.ExternalJsonFile)"/>
</MudStack> </MudStack>
break; break;
case PlayoutKind.Yaml: case PlayoutKind.Sequential:
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"> <div class="d-flex">
<MudText>YAML File</MudText> <MudText>Sequential Schedule</MudText>
</div> </div>
<MudTextField @bind-Value="_model.YamlFile" For="@(() => _model.YamlFile)"/> <MudTextField @bind-Value="_model.SequentialSchedule" For="@(() => _model.SequentialSchedule)"/>
</MudStack> </MudStack>
break; break;
case PlayoutKind.Block: case PlayoutKind.Block:
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"></div>
<MudText Typo="Typo.caption" Style="font-weight: normal">Block templates are added later</MudText>
</MudStack>
break; break;
default: default:
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"> <div class="d-flex">
<MudText>Schedule</MudText> <MudText>Classic Schedule</MudText>
</div> </div>
<MudSelect T="ProgramScheduleViewModel" @bind-value="_model.ProgramSchedule"> <MudSelect T="ProgramScheduleViewModel" @bind-value="_model.ProgramSchedule">
@foreach (ProgramScheduleViewModel schedule in _programSchedules) @foreach (ProgramScheduleViewModel schedule in _programSchedules)

65
ErsatzTV/Pages/Playouts.razor

@ -17,24 +17,17 @@
<div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6"> <div style="display: flex; flex-direction: row; margin-bottom: auto; margin-top: auto; width: 100%; align-items: center" class="ml-6 mr-6">
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<div style="margin-left: auto" class="d-none d-md-flex"> <div style="margin-left: auto" class="d-none d-md-flex">
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Href="playouts/add"> <MudMenu>
Add Playout <ActivatorContent>
</MudButton> <MudButton StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Filled" Color="Color.Primary">Add Playout</MudButton>
<MudTooltip Text="This feature is experimental"> </ActivatorContent>
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Block}")"> <ChildContent>
Add Block Playout <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Classic Schedule" Href="playouts/add"/>
</MudButton> <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Block Schedule" Href="@($"playouts/add/{PlayoutKind.Block}")"/>
</MudTooltip> <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Sequential Schedule" Href="@($"playouts/add/{PlayoutKind.Sequential}")"/>
<MudTooltip Text="This feature is experimental"> <MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="JSON (dizqueTV) Schedule" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")"/>
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.Yaml}")"> </ChildContent>
Add YAML Playout </MudMenu>
</MudButton>
</MudTooltip>
<MudTooltip Text="This feature is experimental">
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Warning" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")">
Add External Json Playout
</MudButton>
</MudTooltip>
<MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@ResetAllPlayouts"> <MudButton Class="ml-3" Variant="Variant.Filled" Color="Color.Warning" StartIcon="@Icons.Material.Filled.Refresh" OnClick="@ResetAllPlayouts">
Reset All Playouts Reset All Playouts
</MudButton> </MudButton>
@ -42,11 +35,11 @@
<div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none"> <div style="align-items: center; display: flex; margin-left: auto;" class="d-md-none">
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>
<MudMenu Icon="@Icons.Material.Filled.MoreVert"> <MudMenu Icon="@Icons.Material.Filled.MoreVert">
<MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Playout" Href="playouts/add"/> <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Classic Playout" Href="playouts/add"/>
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add Block Playout" Href="@($"playouts/add/{PlayoutKind.Block}")"/> <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Block Playout" Href="@($"playouts/add/{PlayoutKind.Block}")"/>
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add YAML Playout" Href="@($"playouts/add/{PlayoutKind.Yaml}")"/> <MudMenuItem Icon="@Icons.Material.Filled.Add" Label="Add Sequential Playout" Href="@($"playouts/add/{PlayoutKind.Sequential}")"/>
<MudMenuItem Icon="@Icons.Material.Filled.Warning" Label="Add External Json Playout" Href="@($"playouts/add/{PlayoutKind.ExternalJson}")"/> <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"/> <MudMenuItem Icon="@Icons.Material.Filled.Refresh" Label="Reset All Playouts" OnClick="@ResetAllPlayouts"/>
</MudMenu> </MudMenu>
</div> </div>
</div> </div>
@ -80,23 +73,23 @@
Default Schedule Default Schedule
</MudTableSortLabel> </MudTableSortLabel>
</MudTh> </MudTh>
<MudTh Class="d-none d-md-table-cell">Playout Type</MudTh> <MudTh Class="d-none d-md-table-cell">Schedule Kind</MudTh>
<MudTh/> <MudTh/>
</HeaderContent> </HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd>@context.ChannelNumber - @context.ChannelName</MudTd> <MudTd>@context.ChannelNumber - @context.ChannelName</MudTd>
<MudTd Class="d-none d-md-table-cell">@context.ScheduleName</MudTd> <MudTd Class="d-none d-md-table-cell">@context.ScheduleName</MudTd>
<MudTd Class="d-none d-md-table-cell"> <MudTd Class="d-none d-md-table-cell">
@switch (context.PlayoutType) @switch (context.ScheduleKind)
{ {
case ProgramSchedulePlayoutType.Block: case PlayoutScheduleKind.Block:
<span>Block</span> <span>Block</span>
break; break;
case ProgramSchedulePlayoutType.Yaml: case PlayoutScheduleKind.Sequential:
<span>YAML</span> <span>Sequential</span>
break; break;
case ProgramSchedulePlayoutType.ExternalJson: case PlayoutScheduleKind.ExternalJson:
<span>External Json</span> <span>JSON (dizqueTV)</span>
break; break;
default: default:
<span></span> <span></span>
@ -111,7 +104,7 @@
<MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/> <MudProgressCircular Color="Color.Primary" Size="Size.Small" Indeterminate="true"/>
} }
</div> </div>
@if (context.PlayoutType == ProgramSchedulePlayoutType.Classic) @if (context.ScheduleKind == PlayoutScheduleKind.Classic)
{ {
if (context.PlayoutMode is ChannelPlayoutMode.OnDemand) if (context.PlayoutMode is ChannelPlayoutMode.OnDemand)
{ {
@ -144,7 +137,7 @@
</MudIconButton> </MudIconButton>
</MudTooltip> </MudTooltip>
} }
else if (context.PlayoutType == ProgramSchedulePlayoutType.ExternalJson) else if (context.ScheduleKind == PlayoutScheduleKind.ExternalJson)
{ {
<MudTooltip Text="Edit External Json File"> <MudTooltip Text="Edit External Json File">
<MudIconButton Icon="@Icons.Material.Filled.Edit" <MudIconButton Icon="@Icons.Material.Filled.Edit"
@ -155,7 +148,7 @@
<div style="width: 48px"></div> <div style="width: 48px"></div>
<div style="width: 48px"></div> <div style="width: 48px"></div>
} }
else if (context.PlayoutType == ProgramSchedulePlayoutType.Yaml) else if (context.ScheduleKind == PlayoutScheduleKind.Sequential)
{ {
<MudTooltip Text="Edit Playout"> <MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit" <MudIconButton Icon="@Icons.Material.Filled.Edit"
@ -171,7 +164,7 @@
</MudTooltip> </MudTooltip>
<div style="width: 48px"></div> <div style="width: 48px"></div>
} }
else if (context.PlayoutType == ProgramSchedulePlayoutType.Block) else if (context.ScheduleKind == PlayoutScheduleKind.Block)
{ {
<MudTooltip Text="Edit Playout"> <MudTooltip Text="Edit Playout">
<MudIconButton Icon="@Icons.Material.Filled.Edit" <MudIconButton Icon="@Icons.Material.Filled.Edit"
@ -297,8 +290,8 @@
private async Task PlayoutSelected(PlayoutNameViewModel playout) private async Task PlayoutSelected(PlayoutNameViewModel playout)
{ {
// only show details for flood, block and YAML playouts // only show details for flood, block and sequential playouts
_selectedPlayoutId = playout.PlayoutType is ProgramSchedulePlayoutType.Classic or ProgramSchedulePlayoutType.Block or ProgramSchedulePlayoutType.Yaml _selectedPlayoutId = playout.ScheduleKind is PlayoutScheduleKind.Classic or PlayoutScheduleKind.Block or PlayoutScheduleKind.Sequential
? playout.PlayoutId ? playout.PlayoutId
: null; : null;

16
ErsatzTV/Pages/YamlPlayoutEditor.razor → ErsatzTV/Pages/SequentialPlayoutEditor.razor

@ -7,21 +7,21 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IMediator Mediator @inject IMediator Mediator
@inject IEntityLocker EntityLocker; @inject IEntityLocker EntityLocker;
@inject ILogger<YamlPlayoutEditor> Logger @inject ILogger<SequentialPlayoutEditor> Logger
<MudForm Style="max-height: 100%"> <MudForm Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center"> <MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save"> <MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="@(_ => SaveChanges())" StartIcon="@Icons.Material.Filled.Save">
Save YAML File Save Sequential Schedule
</MudButton> </MudButton>
</MudPaper> </MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto"> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">@_channelName - YAML Playout</MudText> <MudText Typo="Typo.h5" Class="mb-2">@_channelName - Sequential Schedule</MudText>
<MudDivider Class="mb-6"/> <MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> <MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex"> <div class="d-flex">
<MudText>YAML File</MudText> <MudText>Sequential Schedule</MudText>
</div> </div>
<MudTextField @bind-Value="@_playout.TemplateFile" For="@(() => _playout.TemplateFile)"/> <MudTextField @bind-Value="@_playout.TemplateFile" For="@(() => _playout.TemplateFile)"/>
</MudStack> </MudStack>
@ -92,14 +92,14 @@
} }
Either<BaseError, PlayoutNameViewModel> result = Either<BaseError, PlayoutNameViewModel> result =
await Mediator.Send(new UpdateYamlPlayout(_playout.PlayoutId, _playout.TemplateFile), _cts.Token); await Mediator.Send(new UpdateSequentialPlayout(_playout.PlayoutId, _playout.TemplateFile), _cts.Token);
result.Match( result.Match(
_ => { Snackbar.Add($"Saved YAML file for playout {_channelName}", Severity.Success); }, _ => { Snackbar.Add($"Saved sequential schedule for playout {_channelName}", Severity.Success); },
error => error =>
{ {
Snackbar.Add($"Unexpected error saving YAML file: {error.Value}", Severity.Error); Snackbar.Add($"Unexpected error saving sequential schedule: {error.Value}", Severity.Error);
Logger.LogError("Unexpected error saving YAML file: {Error}", error.Value); Logger.LogError("Unexpected error saving sequential schedule: {Error}", error.Value);
}); });
} }

6
ErsatzTV/Pages/YamlValidator.razor

@ -1,7 +1,7 @@
@page "/system/troubleshooting/yaml" @page "/system/troubleshooting/yaml"
@using ErsatzTV.Core.Interfaces.Scheduling @using ErsatzTV.Core.Interfaces.Scheduling
@implements IDisposable @implements IDisposable
@inject IYamlScheduleValidator YamlScheduleValidator @inject ISequentialScheduleValidator SequentialScheduleValidator
<MudForm Style="max-height: 100%"> <MudForm Style="max-height: 100%">
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto"> <div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
@ -99,8 +99,8 @@
_yamlText = await File.ReadAllTextAsync(_yamlFile); _yamlText = await File.ReadAllTextAsync(_yamlFile);
try try
{ {
_jsonText = YamlScheduleValidator.ToJson(_yamlText); _jsonText = SequentialScheduleValidator.ToJson(_yamlText);
IList<string> messages = await YamlScheduleValidator.GetValidationMessages(_yamlText, _isImport); IList<string> messages = await SequentialScheduleValidator.GetValidationMessages(_yamlText, _isImport);
_messagesCount = messages.Count; _messagesCount = messages.Count;
_messages = string.Join("\n", messages); _messages = string.Join("\n", messages);

2
ErsatzTV/PlayoutKind.cs

@ -3,6 +3,6 @@ namespace ErsatzTV;
public static class PlayoutKind public static class PlayoutKind
{ {
public const string ExternalJson = "externaljson"; public const string ExternalJson = "externaljson";
public const string Yaml = "yaml"; public const string Sequential = "sequential";
public const string Block = "block"; public const string Block = "block";
} }

6
ErsatzTV/Resources/yaml-playout-import.schema.json → ErsatzTV/Resources/sequential-schedule-import.schema.json

@ -1,8 +1,8 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ersatztv.org/yaml-playout.schema.json", "$id": "https://ersatztv.org/sequential-schedule.schema.json",
"title": "YAML Playout Import", "title": "Sequential Schedule Import",
"description": "An ErsatzTV YAML playout import definition", "description": "An ErsatzTV sequential schedule import definition",
"type": "object", "type": "object",
"properties": { "properties": {
"content": { "content": {

6
ErsatzTV/Resources/yaml-playout.schema.json → ErsatzTV/Resources/sequential-schedule.schema.json

@ -1,8 +1,8 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://ersatztv.org/yaml-playout.schema.json", "$id": "https://ersatztv.org/sequential-schedule.schema.json",
"title": "YAML Playout", "title": "Sequential Schedule",
"description": "An ErsatzTV YAML playout definition", "description": "An ErsatzTV sequential schedule definition",
"type": "object", "type": "object",
"properties": { "properties": {
"import": { "import": {

4
ErsatzTV/Services/RunOnce/ResourceExtractorService.cs

@ -24,8 +24,8 @@ public class ResourceExtractorService : BackgroundService
await ExtractResource(assembly, "song_progress_overlay.png", stoppingToken); await ExtractResource(assembly, "song_progress_overlay.png", stoppingToken);
await ExtractResource(assembly, "song_progress_overlay_43.png", stoppingToken); await ExtractResource(assembly, "song_progress_overlay_43.png", stoppingToken);
await ExtractResource(assembly, "ErsatzTV.png", stoppingToken); await ExtractResource(assembly, "ErsatzTV.png", stoppingToken);
await ExtractResource(assembly, "yaml-playout.schema.json", stoppingToken); await ExtractResource(assembly, "sequential-schedule.schema.json", stoppingToken);
await ExtractResource(assembly, "yaml-playout-import.schema.json", stoppingToken); await ExtractResource(assembly, "sequential-schedule-import.schema.json", stoppingToken);
await ExtractFontResource(assembly, "Sen.ttf", stoppingToken); await ExtractFontResource(assembly, "Sen.ttf", stoppingToken);
await ExtractFontResource(assembly, "Roboto-Regular.ttf", stoppingToken); await ExtractFontResource(assembly, "Roboto-Regular.ttf", stoppingToken);

2
ErsatzTV/Startup.cs

@ -745,7 +745,7 @@ public class Startup
services.AddScoped<IJellyfinSecretStore, JellyfinSecretStore>(); services.AddScoped<IJellyfinSecretStore, JellyfinSecretStore>();
services.AddScoped<IEmbySecretStore, EmbySecretStore>(); services.AddScoped<IEmbySecretStore, EmbySecretStore>();
services.AddScoped<IScriptEngine, ScriptEngine>(); services.AddScoped<IScriptEngine, ScriptEngine>();
services.AddScoped<IYamlScheduleValidator, YamlScheduleValidator>(); services.AddScoped<ISequentialScheduleValidator, SequentialScheduleValidator>();
services.AddScoped<PlexEtag>(); services.AddScoped<PlexEtag>();

6
ErsatzTV/ViewModels/PlayoutEditViewModel.cs

@ -10,14 +10,14 @@ public class PlayoutEditViewModel
public ChannelViewModel Channel { get; set; } public ChannelViewModel Channel { get; set; }
public ProgramScheduleViewModel ProgramSchedule { get; set; } public ProgramScheduleViewModel ProgramSchedule { get; set; }
public string ExternalJsonFile { get; set; } public string ExternalJsonFile { get; set; }
public string YamlFile { get; set; } public string SequentialSchedule { get; set; }
public CreatePlayout ToCreate() => public CreatePlayout ToCreate() =>
Kind switch Kind switch
{ {
PlayoutKind.ExternalJson => new CreateExternalJsonPlayout(Channel.Id, ExternalJsonFile), PlayoutKind.ExternalJson => new CreateExternalJsonPlayout(Channel.Id, ExternalJsonFile),
PlayoutKind.Yaml => new CreateYamlPlayout(Channel.Id, YamlFile), PlayoutKind.Sequential => new CreateSequentialPlayout(Channel.Id, SequentialSchedule),
PlayoutKind.Block => new CreateBlockPlayout(Channel.Id), PlayoutKind.Block => new CreateBlockPlayout(Channel.Id),
_ => new CreateFloodPlayout(Channel.Id, ProgramSchedule.Id) _ => new CreateClassicPlayout(Channel.Id, ProgramSchedule.Id)
}; };
} }

Loading…
Cancel
Save