diff --git a/ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs index 1ea848ea2..81eea96fd 100644 --- a/ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/UpdateExternalJsonPlayoutHandler.cs @@ -54,7 +54,9 @@ public class playout.ProgramSchedule?.Name ?? string.Empty, playout.ScheduleFile, playout.DailyRebuildTime, - playout.BuildStatus); + playout.BuildStatus, + playout.DecoId, + playout.Deco?.Name); } private static Task> Validate( diff --git a/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs index c01238705..1ca83a979 100644 --- a/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/UpdatePlayoutHandler.cs @@ -46,7 +46,9 @@ public class UpdatePlayoutHandler : IRequestHandler> Validate( diff --git a/ErsatzTV.Application/Playouts/Commands/UpdateScriptedPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/UpdateScriptedPlayoutHandler.cs index 805a48214..329fc33b3 100644 --- a/ErsatzTV.Application/Playouts/Commands/UpdateScriptedPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/UpdateScriptedPlayoutHandler.cs @@ -49,7 +49,9 @@ public class playout.ProgramSchedule?.Name ?? string.Empty, playout.ScheduleFile, playout.DailyRebuildTime, - playout.BuildStatus); + playout.BuildStatus, + playout.DecoId, + playout.Deco?.Name); } private async Task> Validate( diff --git a/ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs b/ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs index 802134c32..1f50282ab 100644 --- a/ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs +++ b/ErsatzTV.Application/Playouts/Commands/UpdateSequentialPlayoutHandler.cs @@ -54,7 +54,9 @@ public class playout.ProgramSchedule?.Name ?? string.Empty, playout.ScheduleFile, playout.DailyRebuildTime, - playout.BuildStatus); + playout.BuildStatus, + playout.DecoId, + playout.Deco?.Name); } private static Task> Validate( diff --git a/ErsatzTV.Application/Playouts/Mapper.cs b/ErsatzTV.Application/Playouts/Mapper.cs index 679ef7900..18d10475e 100644 --- a/ErsatzTV.Application/Playouts/Mapper.cs +++ b/ErsatzTV.Application/Playouts/Mapper.cs @@ -15,7 +15,11 @@ internal static class Mapper playout.ProgramScheduleId == null ? string.Empty : playout.ProgramSchedule.Name, playout.ScheduleFile, playout.DailyRebuildTime, - playout.BuildStatus); + playout.BuildStatus, + playout.DecoId, + // the paged-playouts query does not eager-load Deco (the list response does not surface + // the default deco); GetPlayoutById includes it for the detail response + playout.Deco?.Name); internal static PlayoutItemViewModel ProjectToViewModel(PlayoutItem playoutItem) => new( diff --git a/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs b/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs index 6abde61f1..6ee27fbf0 100644 --- a/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs +++ b/ErsatzTV.Application/Playouts/PlayoutNameViewModel.cs @@ -11,7 +11,9 @@ public record PlayoutNameViewModel( string ScheduleName, string ScheduleFile, TimeSpan? DbDailyRebuildTime, - PlayoutBuildStatus BuildStatus) + PlayoutBuildStatus BuildStatus, + int? DecoId, + string DecoName) { public Option DailyRebuildTime => Optional(DbDailyRebuildTime); diff --git a/ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs b/ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs index b765ff7dd..8583f91ac 100644 --- a/ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs +++ b/ErsatzTV.Application/Playouts/Queries/GetPlayoutByIdHandler.cs @@ -17,6 +17,7 @@ public class GetPlayoutByIdHandler(IDbContextFactory dbContextFactory .Include(p => p.ProgramSchedule) .Include(p => p.Channel) .Include(p => p.BuildStatus) + .Include(p => p.Deco) .SelectOneAsync(p => p.Id, p => p.Id == request.PlayoutId, cancellationToken) .MapT(p => new PlayoutNameViewModel( p.Id, @@ -27,6 +28,8 @@ public class GetPlayoutByIdHandler(IDbContextFactory dbContextFactory p.ProgramScheduleId == null ? string.Empty : p.ProgramSchedule.Name, p.ScheduleFile, p.DailyRebuildTime, - p.BuildStatus)); + p.BuildStatus, + p.DecoId, + p.DecoId == null ? null : p.Deco.Name)); } } diff --git a/ErsatzTV.Core/Api/Playouts/PlayoutAlternateScheduleResponseModel.cs b/ErsatzTV.Core/Api/Playouts/PlayoutAlternateScheduleResponseModel.cs new file mode 100644 index 000000000..1ca1ca38e --- /dev/null +++ b/ErsatzTV.Core/Api/Playouts/PlayoutAlternateScheduleResponseModel.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace ErsatzTV.Core.Api.Playouts; + +public record PlayoutAlternateScheduleResponseModel( + int Id, + int Index, + int ProgramScheduleId, + ICollection DaysOfWeek, + ICollection DaysOfMonth, + ICollection MonthsOfYear, + bool LimitToDateRange, + int StartMonth, + int StartDay, + int? StartYear, + int EndMonth, + int EndDay, + int? EndYear); diff --git a/ErsatzTV.Core/Api/Playouts/PlayoutResponseModel.cs b/ErsatzTV.Core/Api/Playouts/PlayoutResponseModel.cs index cd382a278..63b08b96b 100644 --- a/ErsatzTV.Core/Api/Playouts/PlayoutResponseModel.cs +++ b/ErsatzTV.Core/Api/Playouts/PlayoutResponseModel.cs @@ -12,7 +12,9 @@ public record PlayoutResponseModel( string ScheduleName, string? ScheduleFile, TimeSpan? DailyRebuildTime, - PlayoutBuildStatusResponseModel? BuildStatus) + PlayoutBuildStatusResponseModel? BuildStatus, + int? DecoId, + string? DecoName) { public static PlayoutResponseModel From( int id, @@ -23,7 +25,9 @@ public record PlayoutResponseModel( string scheduleName, string? scheduleFile, TimeSpan? dailyRebuildTime, - PlayoutBuildStatusResponseModel? buildStatus) => + PlayoutBuildStatusResponseModel? buildStatus, + int? decoId, + string? decoName) => new( id, scheduleKind, @@ -33,5 +37,7 @@ public record PlayoutResponseModel( scheduleName, scheduleFile, dailyRebuildTime, - buildStatus); + buildStatus, + decoId, + decoName); } diff --git a/ErsatzTV.Core/Api/Scheduling/PlayoutTemplateResponseModel.cs b/ErsatzTV.Core/Api/Scheduling/PlayoutTemplateResponseModel.cs new file mode 100644 index 000000000..1319f17e2 --- /dev/null +++ b/ErsatzTV.Core/Api/Scheduling/PlayoutTemplateResponseModel.cs @@ -0,0 +1,22 @@ +#nullable enable +namespace ErsatzTV.Core.Api.Scheduling; + +public record PlayoutTemplateResponseModel( + int Id, + int Index, + int TemplateId, + string TemplateName, + string TemplateGroupName, + int? DecoTemplateId, + string? DecoTemplateName, + string? DecoTemplateGroupName, + ICollection DaysOfWeek, + ICollection DaysOfMonth, + ICollection MonthsOfYear, + bool LimitToDateRange, + int StartMonth, + int StartDay, + int? StartYear, + int EndMonth, + int EndDay, + int? EndYear); diff --git a/ErsatzTV.Tests/Controllers/ApiErrorResponseMetadataTests.cs b/ErsatzTV.Tests/Controllers/ApiErrorResponseMetadataTests.cs index d0d94433d..7ab30a490 100644 --- a/ErsatzTV.Tests/Controllers/ApiErrorResponseMetadataTests.cs +++ b/ErsatzTV.Tests/Controllers/ApiErrorResponseMetadataTests.cs @@ -121,6 +121,14 @@ public class ApiErrorResponseMetadataTests [TestCase(typeof(PlayoutController), nameof(PlayoutController.Update), StatusCodes.Status422UnprocessableEntity)] [TestCase(typeof(PlayoutController), nameof(PlayoutController.Delete), StatusCodes.Status404NotFound)] [TestCase(typeof(PlayoutController), nameof(PlayoutController.Delete), StatusCodes.Status422UnprocessableEntity)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.GetAlternateSchedules), StatusCodes.Status404NotFound)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.GetAlternateSchedules), StatusCodes.Status422UnprocessableEntity)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.ReplaceAlternateSchedules), StatusCodes.Status404NotFound)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.ReplaceAlternateSchedules), StatusCodes.Status422UnprocessableEntity)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.GetTemplates), StatusCodes.Status404NotFound)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.GetTemplates), StatusCodes.Status422UnprocessableEntity)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.ReplaceTemplates), StatusCodes.Status404NotFound)] + [TestCase(typeof(PlayoutController), nameof(PlayoutController.ReplaceTemplates), StatusCodes.Status422UnprocessableEntity)] [TestCase(typeof(FFmpegProfileController), nameof(FFmpegProfileController.GetById), StatusCodes.Status404NotFound)] [TestCase(typeof(FFmpegProfileController), nameof(FFmpegProfileController.AddOne), StatusCodes.Status404NotFound)] [TestCase(typeof(FFmpegProfileController), nameof(FFmpegProfileController.AddOne), StatusCodes.Status401Unauthorized)] diff --git a/ErsatzTV.Tests/Controllers/ChannelControllerTests.cs b/ErsatzTV.Tests/Controllers/ChannelControllerTests.cs index 3c23a0b4d..9377d2f7c 100644 --- a/ErsatzTV.Tests/Controllers/ChannelControllerTests.cs +++ b/ErsatzTV.Tests/Controllers/ChannelControllerTests.cs @@ -398,6 +398,8 @@ public class ChannelControllerTests "Schedule", string.Empty, null, + null, + null, null); private static ChannelViewModel MakeVm(int id) => diff --git a/ErsatzTV.Tests/Controllers/PlayoutControllerTests.cs b/ErsatzTV.Tests/Controllers/PlayoutControllerTests.cs index ff57c40d0..23811eb96 100644 --- a/ErsatzTV.Tests/Controllers/PlayoutControllerTests.cs +++ b/ErsatzTV.Tests/Controllers/PlayoutControllerTests.cs @@ -1,12 +1,16 @@ using System.Reflection; using ErsatzTV.Application.Playouts; +using ErsatzTV.Application.ProgramSchedules; +using ErsatzTV.Application.Scheduling; using ErsatzTV.Controllers.Api; using ErsatzTV.Controllers.Api.Requests; using ErsatzTV.Core; using ErsatzTV.Core.Api.Playouts; +using ErsatzTV.Core.Api.Scheduling; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Core.Errors; +using ErsatzTV.Core.Scheduling; using LanguageExt; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -43,6 +47,10 @@ public class PlayoutControllerTests ShouldHaveActionRoute(nameof(PlayoutController.Update), "PUT", "/api/playouts/{id:int}"); ShouldHaveActionRoute(nameof(PlayoutController.ResetAll), "POST", "/api/playouts/reset-all"); ShouldHaveActionRoute(nameof(PlayoutController.Delete), "DELETE", "/api/playouts/{id:int}"); + ShouldHaveActionRoute(nameof(PlayoutController.GetAlternateSchedules), "GET", "/api/playouts/{id:int}/alternate-schedules"); + ShouldHaveActionRoute(nameof(PlayoutController.ReplaceAlternateSchedules), "PUT", "/api/playouts/{id:int}/alternate-schedules"); + ShouldHaveActionRoute(nameof(PlayoutController.GetTemplates), "GET", "/api/playouts/{id:int}/templates"); + ShouldHaveActionRoute(nameof(PlayoutController.ReplaceTemplates), "PUT", "/api/playouts/{id:int}/templates"); } [Test] @@ -448,6 +456,318 @@ public class PlayoutControllerTests await _mediator.Received(1).Send(Arg.Any(), Arg.Any()); } + // ----- Alternate schedules ----- + + [Test] + public async Task GetAlternateSchedules_Should_Return_404_When_Playout_Missing() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.None); + + IActionResult result = await _controller.GetAlternateSchedules(404, CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task GetAlternateSchedules_Should_Return_422_For_NonClassic_Playout() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block })); + + IActionResult result = await _controller.GetAlternateSchedules(9, CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task GetAlternateSchedules_Should_Project_Ordered_List() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9))); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeAltVm(2, 1, 8), MakeAltVm(1, 0, 7)]); + + IActionResult actionResult = await _controller.GetAlternateSchedules(9, CancellationToken.None); + + var list = actionResult.ShouldBeOfType().Value + .ShouldBeOfType>(); + list.Count.ShouldBe(2); + list[0].Index.ShouldBe(0); + list[0].ProgramScheduleId.ShouldBe(7); + list[1].Index.ShouldBe(1); + list[1].ProgramScheduleId.ShouldBe(8); + } + + [Test] + public async Task ReplaceAlternateSchedules_Should_Return_404_When_Playout_Missing() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.None); + + IActionResult result = await _controller.ReplaceAlternateSchedules( + 404, + new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive() + .Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceAlternateSchedules_Should_Return_422_For_NonClassic_Playout() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block })); + + IActionResult result = await _controller.ReplaceAlternateSchedules( + 9, + new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive() + .Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceAlternateSchedules_Should_Return_422_When_Items_Empty() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9))); + + IActionResult result = await _controller.ReplaceAlternateSchedules( + 9, + new ReplacePlayoutAlternateSchedulesRequest([]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive() + .Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceAlternateSchedules_Should_Return_422_When_ProgramScheduleId_Unknown() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9))); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeScheduleVm(7)]); + + IActionResult result = await _controller.ReplaceAlternateSchedules( + 9, + new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7), MakeAltRequest(999)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive() + .Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceAlternateSchedules_Should_Assign_Index_From_Order_And_Return_200() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9))); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeScheduleVm(7), MakeScheduleVm(8)]); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Right(Unit.Default)); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeAltVm(1, 0, 7), MakeAltVm(2, 1, 8)]); + + IActionResult result = await _controller.ReplaceAlternateSchedules( + 9, + new ReplacePlayoutAlternateSchedulesRequest([MakeAltRequest(7), MakeAltRequest(8)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.Received(1).Send( + Arg.Is(c => + c.PlayoutId == 9 && + c.Items.Count == 2 && + c.Items[0].Index == 0 && c.Items[0].ProgramScheduleId == 7 && + c.Items[1].Index == 1 && c.Items[1].ProgramScheduleId == 8), + Arg.Any()); + } + + // ----- Playout templates ----- + + [Test] + public async Task GetTemplates_Should_Return_404_When_Playout_Missing() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.None); + + IActionResult result = await _controller.GetTemplates(404, CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task GetTemplates_Should_Return_422_For_NonBlock_Playout() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9))); + + IActionResult result = await _controller.GetTemplates(9, CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task GetTemplates_Should_Project_Ordered_List() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block })); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeTemplateVm(2, 1, 8, 5), MakeTemplateVm(1, 0, 7, null)]); + + IActionResult actionResult = await _controller.GetTemplates(9, CancellationToken.None); + + var list = actionResult.ShouldBeOfType().Value + .ShouldBeOfType>(); + list.Count.ShouldBe(2); + list[0].Index.ShouldBe(0); + list[0].TemplateId.ShouldBe(7); + list[0].DecoTemplateId.ShouldBeNull(); + list[1].Index.ShouldBe(1); + list[1].TemplateId.ShouldBe(8); + list[1].DecoTemplateId.ShouldBe(5); + } + + [Test] + public async Task ReplaceTemplates_Should_Return_404_When_Playout_Missing() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.None); + + IActionResult result = await _controller.ReplaceTemplates( + 404, + new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, null)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceTemplates_Should_Return_422_For_NonBlock_Playout() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9))); + + IActionResult result = await _controller.ReplaceTemplates( + 9, + new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, null)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceTemplates_Should_Return_422_When_TemplateId_Unknown() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block })); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeTemplateViewModel(7)]); + + IActionResult result = await _controller.ReplaceTemplates( + 9, + new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(999, null)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceTemplates_Should_Return_422_When_DecoTemplateId_Unknown() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block })); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeTemplateViewModel(7)]); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.None); + + IActionResult result = await _controller.ReplaceTemplates( + 9, + new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, 999)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.DidNotReceive().Send(Arg.Any(), Arg.Any()); + } + + [Test] + public async Task ReplaceTemplates_Should_Assign_Index_From_Order_And_Return_200() + { + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(MakePlayout(9) with { ScheduleKind = PlayoutScheduleKind.Block })); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeTemplateViewModel(7), MakeTemplateViewModel(8)]); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.Some(new DecoTemplateViewModel(5, 1, "G", "DT"))); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns(Option.None); + _mediator.Send(Arg.Any(), Arg.Any()) + .Returns([MakeTemplateVm(1, 0, 7, null), MakeTemplateVm(2, 1, 8, 5)]); + + IActionResult result = await _controller.ReplaceTemplates( + 9, + new ReplacePlayoutTemplatesRequest([MakeTemplateRequest(7, null), MakeTemplateRequest(8, 5)]), + CancellationToken.None); + + result.ShouldBeOfType(); + await _mediator.Received(1).Send( + Arg.Is(c => + c.PlayoutId == 9 && + c.Items.Count == 2 && + c.Items[0].Index == 0 && c.Items[0].TemplateId == 7 && c.Items[0].DecoTemplateId == null && + c.Items[1].Index == 1 && c.Items[1].TemplateId == 8 && c.Items[1].DecoTemplateId == 5), + Arg.Any()); + } + + private static PlayoutAlternateScheduleViewModel MakeAltVm(int id, int index, int programScheduleId) => + new(id, index, programScheduleId, [], [], [], false, 1, 1, null, 12, 31, null); + + private static PlayoutAlternateScheduleItemRequest MakeAltRequest(int programScheduleId) => + new(0, programScheduleId, [], [], [], false, 1, 1, null, 12, 31, null); + + private static ProgramScheduleViewModel MakeScheduleVm(int id) => + new(id, $"Schedule {id}", false, false, false, false, FixedStartTimeBehavior.Strict); + + private static TemplateViewModel MakeTemplateViewModel(int id) => + new(id, 1, "Group", $"Template {id}"); + + private static PlayoutTemplateViewModel MakeTemplateVm(int id, int index, int templateId, int? decoTemplateId) => + new( + id, + new TemplateViewModel(templateId, 1, "Group", $"Template {templateId}"), + decoTemplateId is { } dtId ? new DecoTemplateViewModel(dtId, 1, "DGroup", $"Deco {dtId}") : null, + index, + [], + [], + [], + false, + 1, + 1, + null, + 12, + 31, + null); + + private static PlayoutTemplateItemRequest MakeTemplateRequest(int templateId, int? decoTemplateId) => + new(0, templateId, decoTemplateId, [], [], [], false, 1, 1, null, 12, 31, null); + private static PlayoutNameViewModel MakePlayout(int id) => new( id, @@ -458,7 +778,9 @@ public class PlayoutControllerTests "Schedule", string.Empty, null, - new PlayoutBuildStatus()); + new PlayoutBuildStatus(), + null, + null); private static PlayoutResponseModel ToResponse(PlayoutNameViewModel vm) => PlayoutResponseModel.From( @@ -475,7 +797,9 @@ public class PlayoutControllerTests : new PlayoutBuildStatusResponseModel( vm.BuildStatus.LastBuild, vm.BuildStatus.Success, - vm.BuildStatus.Message)); + vm.BuildStatus.Message), + vm.DecoId, + vm.DecoName); private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route) { diff --git a/ErsatzTV/Controllers/Api/PlayoutController.cs b/ErsatzTV/Controllers/Api/PlayoutController.cs index 00f0c441f..2b9d0d51b 100644 --- a/ErsatzTV/Controllers/Api/PlayoutController.cs +++ b/ErsatzTV/Controllers/Api/PlayoutController.cs @@ -1,9 +1,11 @@ using System.ComponentModel.DataAnnotations; using ErsatzTV.Application.Playouts; +using ErsatzTV.Application.ProgramSchedules; using ErsatzTV.Application.Scheduling; using ErsatzTV.Controllers.Api.Requests; using ErsatzTV.Core; using ErsatzTV.Core.Api.Playouts; +using ErsatzTV.Core.Api.Scheduling; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Domain.Filler; using ErsatzTV.Extensions; @@ -234,6 +236,190 @@ public class PlayoutController(IMediator mediator) : ControllerBase None: () => ApiResults.NotFoundProblem()); } + [HttpGet("/api/playouts/{id:int}/alternate-schedules", Name = "GetPlayoutAlternateSchedules")] + [Tags("Playouts")] + [EndpointSummary("Get a classic playout's alternate schedules")] + [EndpointDescription( + "Lists a Classic playout's alternate schedules in priority order (first = highest priority). The last " + + "entry is the catch-all default, and its schedule is the playout's default schedule; if no explicit " + + "catch-all exists the query synthesizes one from the playout's default schedule. Only valid for Classic " + + "playouts; other kinds return 422.")] + [EndpointGroupName("general")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] + public async Task GetAlternateSchedules(int id, CancellationToken cancellationToken) + { + Option maybePlayout = await mediator.Send(new GetPlayoutById(id), cancellationToken); + if (maybePlayout.IsNone) + { + return ApiResults.NotFoundProblem(); + } + + foreach (PlayoutNameViewModel playout in maybePlayout) + { + if (playout.ScheduleKind is not PlayoutScheduleKind.Classic) + { + return BaseError.New("[AlternateSchedules] are only valid for Classic playouts").ToErrorResult(); + } + } + + List items = + await mediator.Send(new GetPlayoutAlternateSchedules(id), cancellationToken); + return new OkObjectResult(items.OrderBy(i => i.Index).Select(ToResponse).ToList()); + } + + [HttpPut("/api/playouts/{id:int}/alternate-schedules")] + [Tags("Playouts")] + [EndpointSummary("Replace a classic playout's alternate schedules")] + [EndpointDescription( + "Replaces a Classic playout's alternate schedules. Items are ordered by priority: the first item is the " + + "highest priority and the last item is the catch-all default whose schedule becomes the playout's default " + + "schedule. Index is assigned from array order (the request body has no Index field). The list must contain " + + "at least one item, and every ProgramScheduleId must exist. Only valid for Classic playouts.")] + [EndpointGroupName("general")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] + public async Task ReplaceAlternateSchedules( + int id, + [Required] [FromBody] ReplacePlayoutAlternateSchedulesRequest request, + CancellationToken cancellationToken) + { + Option maybePlayout = await mediator.Send(new GetPlayoutById(id), cancellationToken); + if (maybePlayout.IsNone) + { + return ApiResults.NotFoundProblem(); + } + + foreach (PlayoutNameViewModel playout in maybePlayout) + { + if (playout.ScheduleKind is not PlayoutScheduleKind.Classic) + { + return BaseError.New("[AlternateSchedules] are only valid for Classic playouts").ToErrorResult(); + } + } + + List items = request.Items ?? []; + if (items.Count == 0) + { + return BaseError.New("[Items] must contain at least one alternate schedule").ToErrorResult(); + } + + List schedules = + await mediator.Send(new GetAllProgramSchedules(), cancellationToken); + var scheduleIds = schedules.Select(s => s.Id).ToHashSet(); + var missingScheduleIds = items.Select(i => i.ProgramScheduleId).Distinct() + .Where(scheduleId => !scheduleIds.Contains(scheduleId)).ToList(); + if (missingScheduleIds.Count > 0) + { + return BaseError.New($"[ProgramScheduleId] {missingScheduleIds[0]} does not exist").ToErrorResult(); + } + + Either result = await mediator.Send(request.ToCommand(id), cancellationToken); + return await result.Match( + Left: error => Task.FromResult(error.ToErrorResult()), + Right: async _ => + { + List refreshed = + await mediator.Send(new GetPlayoutAlternateSchedules(id), cancellationToken); + return (IActionResult)new OkObjectResult(refreshed.OrderBy(i => i.Index).Select(ToResponse).ToList()); + }); + } + + [HttpGet("/api/playouts/{id:int}/templates", Name = "GetPlayoutTemplates")] + [Tags("Playouts")] + [EndpointSummary("Get a block playout's templates")] + [EndpointDescription( + "Lists a Block playout's templates in priority order (first = highest priority). Each template optionally " + + "carries a deco template. Only valid for Block playouts; other kinds return 422.")] + [EndpointGroupName("general")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] + public async Task GetTemplates(int id, CancellationToken cancellationToken) + { + Option maybePlayout = await mediator.Send(new GetPlayoutById(id), cancellationToken); + if (maybePlayout.IsNone) + { + return ApiResults.NotFoundProblem(); + } + + foreach (PlayoutNameViewModel playout in maybePlayout) + { + if (playout.ScheduleKind is not PlayoutScheduleKind.Block) + { + return BaseError.New("[Templates] are only valid for Block playouts").ToErrorResult(); + } + } + + List items = await mediator.Send(new GetPlayoutTemplates(id), cancellationToken); + return new OkObjectResult(items.OrderBy(i => i.Index).Select(ToResponse).ToList()); + } + + [HttpPut("/api/playouts/{id:int}/templates")] + [Tags("Playouts")] + [EndpointSummary("Replace a block playout's templates")] + [EndpointDescription( + "Replaces a Block playout's templates. Items are ordered by priority (first = highest priority); Index is " + + "assigned from array order (the request body has no Index field). Every TemplateId must exist, and any " + + "supplied DecoTemplateId must exist. An empty list clears all templates. Only valid for Block playouts.")] + [EndpointGroupName("general")] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] + public async Task ReplaceTemplates( + int id, + [Required] [FromBody] ReplacePlayoutTemplatesRequest request, + CancellationToken cancellationToken) + { + Option maybePlayout = await mediator.Send(new GetPlayoutById(id), cancellationToken); + if (maybePlayout.IsNone) + { + return ApiResults.NotFoundProblem(); + } + + foreach (PlayoutNameViewModel playout in maybePlayout) + { + if (playout.ScheduleKind is not PlayoutScheduleKind.Block) + { + return BaseError.New("[Templates] are only valid for Block playouts").ToErrorResult(); + } + } + + List items = request.Items ?? []; + + List templates = await mediator.Send(new GetAllTemplates(), cancellationToken); + var templateIds = templates.Select(t => t.Id).ToHashSet(); + var missingTemplateIds = items.Select(i => i.TemplateId).Distinct() + .Where(templateId => !templateIds.Contains(templateId)).ToList(); + if (missingTemplateIds.Count > 0) + { + return BaseError.New($"[TemplateId] {missingTemplateIds[0]} does not exist").ToErrorResult(); + } + + var decoTemplateIds = items.Where(i => i.DecoTemplateId.HasValue) + .Select(i => i.DecoTemplateId!.Value).Distinct().ToList(); + foreach (int decoTemplateId in decoTemplateIds) + { + Option maybeDecoTemplate = + await mediator.Send(new GetDecoTemplateById(decoTemplateId), cancellationToken); + if (maybeDecoTemplate.IsNone) + { + return BaseError.New($"[DecoTemplateId] {decoTemplateId} does not exist").ToErrorResult(); + } + } + + Option result = await mediator.Send(request.ToCommand(id), cancellationToken); + foreach (BaseError error in result) + { + return error.ToErrorResult(); + } + + List refreshed = await mediator.Send(new GetPlayoutTemplates(id), cancellationToken); + return new OkObjectResult(refreshed.OrderBy(i => i.Index).Select(ToResponse).ToList()); + } + [HttpPost("/api/playouts/reset-all", Name = "ResetAllPlayouts")] [Tags("Playouts")] [EndpointSummary("Reset all playouts")] @@ -268,7 +454,46 @@ public class PlayoutController(IMediator mediator) : ControllerBase vm.ScheduleName, vm.ScheduleFile, vm.DbDailyRebuildTime, - ToBuildStatus(vm.BuildStatus)); + ToBuildStatus(vm.BuildStatus), + vm.DecoId, + vm.DecoName); + + private static PlayoutAlternateScheduleResponseModel ToResponse(PlayoutAlternateScheduleViewModel vm) => + new( + vm.Id, + vm.Index, + vm.ProgramScheduleId, + vm.DaysOfWeek, + vm.DaysOfMonth, + vm.MonthsOfYear, + vm.LimitToDateRange, + vm.StartMonth, + vm.StartDay, + vm.StartYear, + vm.EndMonth, + vm.EndDay, + vm.EndYear); + + private static PlayoutTemplateResponseModel ToResponse(PlayoutTemplateViewModel vm) => + new( + vm.Id, + vm.Index, + vm.Template.Id, + vm.Template.Name, + vm.Template.GroupName, + vm.DecoTemplate?.Id, + vm.DecoTemplate?.Name, + vm.DecoTemplate?.GroupName, + vm.DaysOfWeek, + vm.DaysOfMonth, + vm.MonthsOfYear, + vm.LimitToDateRange, + vm.StartMonth, + vm.StartDay, + vm.StartYear, + vm.EndMonth, + vm.EndDay, + vm.EndYear); private static PlayoutListItemResponseModel ToListItemResponse(PlayoutNameViewModel vm) => new( diff --git a/ErsatzTV/Controllers/Api/Requests/PlayoutAlternateScheduleItemRequest.cs b/ErsatzTV/Controllers/Api/Requests/PlayoutAlternateScheduleItemRequest.cs new file mode 100644 index 000000000..e165eaafc --- /dev/null +++ b/ErsatzTV/Controllers/Api/Requests/PlayoutAlternateScheduleItemRequest.cs @@ -0,0 +1,34 @@ +using ErsatzTV.Application.Playouts; + +namespace ErsatzTV.Controllers.Api.Requests; + +public record PlayoutAlternateScheduleItemRequest( + int Id, + int ProgramScheduleId, + List DaysOfWeek, + List DaysOfMonth, + List MonthsOfYear, + bool LimitToDateRange, + int StartMonth, + int StartDay, + int? StartYear, + int EndMonth, + int EndDay, + int? EndYear) +{ + public ReplacePlayoutAlternateSchedule ToReplaceItem(int index) => + new( + Id, + index, + ProgramScheduleId, + DaysOfWeek ?? [], + DaysOfMonth ?? [], + MonthsOfYear ?? [], + LimitToDateRange, + StartMonth, + StartDay, + StartYear, + EndMonth, + EndDay, + EndYear); +} diff --git a/ErsatzTV/Controllers/Api/Requests/PlayoutTemplateItemRequest.cs b/ErsatzTV/Controllers/Api/Requests/PlayoutTemplateItemRequest.cs new file mode 100644 index 000000000..540b4f99a --- /dev/null +++ b/ErsatzTV/Controllers/Api/Requests/PlayoutTemplateItemRequest.cs @@ -0,0 +1,36 @@ +using ErsatzTV.Application.Scheduling; + +namespace ErsatzTV.Controllers.Api.Requests; + +public record PlayoutTemplateItemRequest( + int Id, + int TemplateId, + int? DecoTemplateId, + List DaysOfWeek, + List DaysOfMonth, + List MonthsOfYear, + bool LimitToDateRange, + int StartMonth, + int StartDay, + int? StartYear, + int EndMonth, + int EndDay, + int? EndYear) +{ + public ReplacePlayoutTemplate ToReplaceItem(int index) => + new( + Id, + index, + TemplateId, + DecoTemplateId, + DaysOfWeek ?? [], + DaysOfMonth ?? [], + MonthsOfYear ?? [], + LimitToDateRange, + StartMonth, + StartDay, + StartYear, + EndMonth, + EndDay, + EndYear); +} diff --git a/ErsatzTV/Controllers/Api/Requests/ReplacePlayoutAlternateSchedulesRequest.cs b/ErsatzTV/Controllers/Api/Requests/ReplacePlayoutAlternateSchedulesRequest.cs new file mode 100644 index 000000000..b94b3ef89 --- /dev/null +++ b/ErsatzTV/Controllers/Api/Requests/ReplacePlayoutAlternateSchedulesRequest.cs @@ -0,0 +1,17 @@ +using ErsatzTV.Application.Playouts; + +namespace ErsatzTV.Controllers.Api.Requests; + +public record ReplacePlayoutAlternateSchedulesRequest(List Items) +{ + // Index is assigned from array order: the first item is the highest priority and the last item + // is the lowest priority (the catch-all default whose schedule becomes the playout's default + // schedule). This mirrors the Blazor editor, which lists items top-to-bottom in priority order + // and writes the highest-Index item's schedule as the playout default. + public ReplacePlayoutAlternateScheduleItems ToCommand(int playoutId) => + new( + playoutId, + (Items ?? []) + .Select((item, index) => item.ToReplaceItem(index)) + .ToList()); +} diff --git a/ErsatzTV/Controllers/Api/Requests/ReplacePlayoutTemplatesRequest.cs b/ErsatzTV/Controllers/Api/Requests/ReplacePlayoutTemplatesRequest.cs new file mode 100644 index 000000000..73bce1d8c --- /dev/null +++ b/ErsatzTV/Controllers/Api/Requests/ReplacePlayoutTemplatesRequest.cs @@ -0,0 +1,14 @@ +using ErsatzTV.Application.Scheduling; + +namespace ErsatzTV.Controllers.Api.Requests; + +public record ReplacePlayoutTemplatesRequest(List Items) +{ + // Index is assigned from array order (top-to-bottom priority), mirroring the Blazor editor. + public ReplacePlayoutTemplateItems ToCommand(int playoutId) => + new( + playoutId, + (Items ?? []) + .Select((item, index) => item.ToReplaceItem(index)) + .ToList()); +} diff --git a/ErsatzTV/wwwroot/openapi/v1.json b/ErsatzTV/wwwroot/openapi/v1.json index 0def3b884..77259c594 100644 --- a/ErsatzTV/wwwroot/openapi/v1.json +++ b/ErsatzTV/wwwroot/openapi/v1.json @@ -5903,6 +5903,418 @@ } } }, + "/api/playouts/{id}/alternate-schedules": { + "get": { + "tags": [ + "Playouts" + ], + "summary": "Get a classic playout's alternate schedules", + "description": "Lists a Classic playout's alternate schedules in priority order (first = highest priority). The last entry is the catch-all default, and its schedule is the playout's default schedule; if no explicit catch-all exists the query synthesizes one from the playout's default schedule. Only valid for Classic playouts; other kinds return 422.", + "operationId": "GetPlayoutAlternateSchedules", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutAlternateScheduleResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutAlternateScheduleResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutAlternateScheduleResponseModel" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "put": { + "tags": [ + "Playouts" + ], + "summary": "Replace a classic playout's alternate schedules", + "description": "Replaces a Classic playout's alternate schedules. Items are ordered by priority: the first item is the highest priority and the last item is the catch-all default whose schedule becomes the playout's default schedule. Index is assigned from array order (the request body has no Index field). The list must contain at least one item, and every ProgramScheduleId must exist. Only valid for Classic playouts.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutAlternateSchedulesRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutAlternateSchedulesRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutAlternateSchedulesRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutAlternateSchedulesRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutAlternateScheduleResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutAlternateScheduleResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutAlternateScheduleResponseModel" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/api/playouts/{id}/templates": { + "get": { + "tags": [ + "Playouts" + ], + "summary": "Get a block playout's templates", + "description": "Lists a Block playout's templates in priority order (first = highest priority). Each template optionally carries a deco template. Only valid for Block playouts; other kinds return 422.", + "operationId": "GetPlayoutTemplates", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutTemplateResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutTemplateResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutTemplateResponseModel" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + }, + "put": { + "tags": [ + "Playouts" + ], + "summary": "Replace a block playout's templates", + "description": "Replaces a Block playout's templates. Items are ordered by priority (first = highest priority); Index is assigned from array order (the request body has no Index field). Every TemplateId must exist, and any supplied DecoTemplateId must exist. An empty list clears all templates. Only valid for Block playouts.", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutTemplatesRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutTemplatesRequest" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutTemplatesRequest" + } + }, + "application/*+json": { + "schema": { + "$ref": "#/components/schemas/ReplacePlayoutTemplatesRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutTemplateResponseModel" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutTemplateResponseModel" + } + } + }, + "text/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PlayoutTemplateResponseModel" + } + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "422": { + "description": "Unprocessable Entity", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + }, + "text/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, "/api/playouts/reset-all": { "post": { "tags": [ @@ -12983,6 +13395,9 @@ } } }, + "DayOfWeek": { + "type": "integer" + }, "DecoBreakContentRequest": { "required": [ "id", @@ -14889,6 +15304,180 @@ } } }, + "PlayoutAlternateScheduleItemRequest": { + "required": [ + "id", + "programScheduleId", + "daysOfWeek", + "daysOfMonth", + "monthsOfYear", + "limitToDateRange", + "startMonth", + "startDay", + "startYear", + "endMonth", + "endDay", + "endYear" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "programScheduleId": { + "type": "integer", + "format": "int32" + }, + "daysOfWeek": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/DayOfWeek" + } + }, + "daysOfMonth": { + "type": [ + "null", + "array" + ], + "items": { + "type": "integer", + "format": "int32" + } + }, + "monthsOfYear": { + "type": [ + "null", + "array" + ], + "items": { + "type": "integer", + "format": "int32" + } + }, + "limitToDateRange": { + "type": "boolean" + }, + "startMonth": { + "type": "integer", + "format": "int32" + }, + "startDay": { + "type": "integer", + "format": "int32" + }, + "startYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "endMonth": { + "type": "integer", + "format": "int32" + }, + "endDay": { + "type": "integer", + "format": "int32" + }, + "endYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + } + } + }, + "PlayoutAlternateScheduleResponseModel": { + "required": [ + "id", + "index", + "programScheduleId", + "daysOfWeek", + "daysOfMonth", + "monthsOfYear", + "limitToDateRange", + "startMonth", + "startDay", + "startYear", + "endMonth", + "endDay", + "endYear" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "index": { + "type": "integer", + "format": "int32" + }, + "programScheduleId": { + "type": "integer", + "format": "int32" + }, + "daysOfWeek": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DayOfWeek" + } + }, + "daysOfMonth": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "monthsOfYear": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "limitToDateRange": { + "type": "boolean" + }, + "startMonth": { + "type": "integer", + "format": "int32" + }, + "startDay": { + "type": "integer", + "format": "int32" + }, + "startYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "endMonth": { + "type": "integer", + "format": "int32" + }, + "endDay": { + "type": "integer", + "format": "int32" + }, + "endYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + } + } + }, "PlayoutBuildMode": { "enum": [ "Continue", @@ -15028,7 +15617,9 @@ "scheduleName", "scheduleFile", "dailyRebuildTime", - "buildStatus" + "buildStatus", + "decoId", + "decoName" ], "type": "object", "properties": { @@ -15073,6 +15664,19 @@ "$ref": "#/components/schemas/PlayoutBuildStatusResponseModel" } ] + }, + "decoId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "decoName": { + "type": [ + "null", + "string" + ] } } }, @@ -15108,6 +15712,218 @@ } } }, + "PlayoutTemplateItemRequest": { + "required": [ + "id", + "templateId", + "decoTemplateId", + "daysOfWeek", + "daysOfMonth", + "monthsOfYear", + "limitToDateRange", + "startMonth", + "startDay", + "startYear", + "endMonth", + "endDay", + "endYear" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "templateId": { + "type": "integer", + "format": "int32" + }, + "decoTemplateId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "daysOfWeek": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/DayOfWeek" + } + }, + "daysOfMonth": { + "type": [ + "null", + "array" + ], + "items": { + "type": "integer", + "format": "int32" + } + }, + "monthsOfYear": { + "type": [ + "null", + "array" + ], + "items": { + "type": "integer", + "format": "int32" + } + }, + "limitToDateRange": { + "type": "boolean" + }, + "startMonth": { + "type": "integer", + "format": "int32" + }, + "startDay": { + "type": "integer", + "format": "int32" + }, + "startYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "endMonth": { + "type": "integer", + "format": "int32" + }, + "endDay": { + "type": "integer", + "format": "int32" + }, + "endYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + } + } + }, + "PlayoutTemplateResponseModel": { + "required": [ + "id", + "index", + "templateId", + "templateName", + "templateGroupName", + "decoTemplateId", + "decoTemplateName", + "decoTemplateGroupName", + "daysOfWeek", + "daysOfMonth", + "monthsOfYear", + "limitToDateRange", + "startMonth", + "startDay", + "startYear", + "endMonth", + "endDay", + "endYear" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "index": { + "type": "integer", + "format": "int32" + }, + "templateId": { + "type": "integer", + "format": "int32" + }, + "templateName": { + "type": "string" + }, + "templateGroupName": { + "type": "string" + }, + "decoTemplateId": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "decoTemplateName": { + "type": [ + "null", + "string" + ] + }, + "decoTemplateGroupName": { + "type": [ + "null", + "string" + ] + }, + "daysOfWeek": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DayOfWeek" + } + }, + "daysOfMonth": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "monthsOfYear": { + "type": "array", + "items": { + "type": "integer", + "format": "int32" + } + }, + "limitToDateRange": { + "type": "boolean" + }, + "startMonth": { + "type": "integer", + "format": "int32" + }, + "startDay": { + "type": "integer", + "format": "int32" + }, + "startYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + }, + "endMonth": { + "type": "integer", + "format": "int32" + }, + "endDay": { + "type": "integer", + "format": "int32" + }, + "endYear": { + "type": [ + "null", + "integer" + ], + "format": "int32" + } + } + }, "ProblemDetails": { "type": "object", "properties": { @@ -15589,6 +16405,40 @@ } } }, + "ReplacePlayoutAlternateSchedulesRequest": { + "required": [ + "items" + ], + "type": "object", + "properties": { + "items": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/PlayoutAlternateScheduleItemRequest" + } + } + } + }, + "ReplacePlayoutTemplatesRequest": { + "required": [ + "items" + ], + "type": "object", + "properties": { + "items": { + "type": [ + "null", + "array" + ], + "items": { + "$ref": "#/components/schemas/PlayoutTemplateItemRequest" + } + } + } + }, "ReplaceScheduleItemsRequest": { "required": [ "items" diff --git a/web/src/api/generated/v1.d.ts b/web/src/api/generated/v1.d.ts index 62b364b1d..b3dfa5e5c 100644 --- a/web/src/api/generated/v1.d.ts +++ b/web/src/api/generated/v1.d.ts @@ -462,6 +462,7 @@ export interface components { "zIndex": number; "placeWithinSourceContent": boolean; }; + "DayOfWeek": number; "DecoBreakContentRequest": { "id": number; "collectionType": components["schemas"]["CollectionType"]; @@ -817,6 +818,35 @@ export interface components { "playlistGroupId": number; "name": null | string; "isSystem": boolean; + }; + "PlayoutAlternateScheduleItemRequest": { + "id": number; + "programScheduleId": number; + "daysOfWeek": null | Array; + "daysOfMonth": null | Array; + "monthsOfYear": null | Array; + "limitToDateRange": boolean; + "startMonth": number; + "startDay": number; + "startYear": null | number; + "endMonth": number; + "endDay": number; + "endYear": null | number; + }; + "PlayoutAlternateScheduleResponseModel": { + "id": number; + "index": number; + "programScheduleId": number; + "daysOfWeek": Array; + "daysOfMonth": Array; + "monthsOfYear": Array; + "limitToDateRange": boolean; + "startMonth": number; + "startDay": number; + "startYear": null | number; + "endMonth": number; + "endDay": number; + "endYear": null | number; }; "PlayoutBuildMode": "Continue" | "Refresh" | "Reset"; "PlayoutBuildStatusResponseModel": { @@ -851,12 +881,49 @@ export interface components { "scheduleFile": null | string; "dailyRebuildTime": null | string; "buildStatus": null | components["schemas"]["PlayoutBuildStatusResponseModel"]; + "decoId": null | number; + "decoName": null | string; }; "PlayoutScheduleKind": "None" | "Classic" | "Block" | "Sequential" | "Scripted" | "ExternalJson"; "PlayoutSettingsResponseModel": { "daysToBuild": number; "skipMissingItems": boolean; "scriptedScheduleTimeoutSeconds": number; + }; + "PlayoutTemplateItemRequest": { + "id": number; + "templateId": number; + "decoTemplateId": null | number; + "daysOfWeek": null | Array; + "daysOfMonth": null | Array; + "monthsOfYear": null | Array; + "limitToDateRange": boolean; + "startMonth": number; + "startDay": number; + "startYear": null | number; + "endMonth": number; + "endDay": number; + "endYear": null | number; + }; + "PlayoutTemplateResponseModel": { + "id": number; + "index": number; + "templateId": number; + "templateName": string; + "templateGroupName": string; + "decoTemplateId": null | number; + "decoTemplateName": null | string; + "decoTemplateGroupName": null | string; + "daysOfWeek": Array; + "daysOfMonth": Array; + "monthsOfYear": Array; + "limitToDateRange": boolean; + "startMonth": number; + "startDay": number; + "startYear": null | number; + "endMonth": number; + "endDay": number; + "endYear": null | number; }; "ProblemDetails": { "type"?: null | string; @@ -949,6 +1016,12 @@ export interface components { "ReplaceDecoTemplateRequest": { "name": null | string; "items": null | Array; + }; + "ReplacePlayoutAlternateSchedulesRequest": { + "items": null | Array; + }; + "ReplacePlayoutTemplatesRequest": { + "items": null | Array; }; "ReplaceScheduleItemsRequest": { "items": null | Array;