Merge remote-tracking branch 'origin/main' into feat/144-s1-blocks
This commit is contained in:
@@ -83,7 +83,11 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
|
||||
[HttpPost("/api/playouts")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Create a classic playout")]
|
||||
[EndpointSummary("Create a playout")]
|
||||
[EndpointDescription(
|
||||
"Creates a playout of any kind (Classic, Block, Sequential, Scripted, or ExternalJson) for a channel. " +
|
||||
"Classic requires ProgramScheduleId; Sequential/Scripted/ExternalJson require ScheduleFile; Block requires " +
|
||||
"neither. A channel may only have one playout.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(PlayoutResponseModel), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
@@ -92,19 +96,100 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
[Required] [FromBody] CreatePlayoutRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Either<BaseError, CreatePlayoutResponse> result = await mediator.Send(request.ToCommand(), cancellationToken);
|
||||
return await result.Match(
|
||||
Either<BaseError, CreatePlayout> commandOrError = request.ToCommand();
|
||||
return await commandOrError.Match(
|
||||
Left: error => Task.FromResult(error.ToErrorResult()),
|
||||
Right: async created =>
|
||||
Right: async command =>
|
||||
{
|
||||
Option<PlayoutNameViewModel> playout =
|
||||
await mediator.Send(new GetPlayoutById(created.PlayoutId), cancellationToken);
|
||||
return playout.Match(
|
||||
Some: vm => (IActionResult)new CreatedResult($"/api/playouts/{vm.PlayoutId}", ToResponse(vm)),
|
||||
None: () => ApiResults.NotFoundProblem());
|
||||
Either<BaseError, CreatePlayoutResponse> result = await mediator.Send(command, cancellationToken);
|
||||
return await result.Match(
|
||||
Left: error => Task.FromResult(error.ToErrorResult()),
|
||||
Right: async created =>
|
||||
{
|
||||
Option<PlayoutNameViewModel> playout =
|
||||
await mediator.Send(new GetPlayoutById(created.PlayoutId), cancellationToken);
|
||||
return playout.Match(
|
||||
Some: vm => (IActionResult)new CreatedResult($"/api/playouts/{vm.PlayoutId}", ToResponse(vm)),
|
||||
None: () => ApiResults.NotFoundProblem());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/api/playouts/{id:int}")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Update playout scheduling details")]
|
||||
[EndpointDescription(
|
||||
"DailyRebuildTime is always applied; omit it (null) to clear the daily reset. ScheduleFile is only valid " +
|
||||
"for Sequential, Scripted, and ExternalJson playouts; omit it (null) to leave it unchanged.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(PlayoutResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<IActionResult> Update(
|
||||
int id,
|
||||
[Required] [FromBody] UpdatePlayoutDetailsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<PlayoutNameViewModel> maybePlayout = await mediator.Send(new GetPlayoutById(id), cancellationToken);
|
||||
if (maybePlayout.IsNone)
|
||||
{
|
||||
return ApiResults.NotFoundProblem();
|
||||
}
|
||||
|
||||
var hasScheduleFile = !string.IsNullOrWhiteSpace(request.ScheduleFile);
|
||||
foreach (PlayoutNameViewModel playout in maybePlayout)
|
||||
{
|
||||
if (hasScheduleFile && playout.ScheduleKind is not (PlayoutScheduleKind.Sequential
|
||||
or PlayoutScheduleKind.Scripted or PlayoutScheduleKind.ExternalJson))
|
||||
{
|
||||
BaseError error =
|
||||
BaseError.New("[ScheduleFile] is only valid for Sequential, Scripted, or ExternalJson playouts");
|
||||
return error.ToErrorResult();
|
||||
}
|
||||
}
|
||||
|
||||
// the schedule-file update is the only step that can fail after the pre-checks,
|
||||
// so it goes first — a rejected file must not leave DailyRebuildTime applied
|
||||
if (hasScheduleFile)
|
||||
{
|
||||
foreach (PlayoutNameViewModel playout in maybePlayout)
|
||||
{
|
||||
Either<BaseError, PlayoutNameViewModel> scheduleFileResult =
|
||||
await UpdateScheduleFile(playout, request.ScheduleFile, cancellationToken);
|
||||
foreach (BaseError error in scheduleFileResult.LeftToSeq())
|
||||
{
|
||||
return error.ToErrorResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Option<TimeSpan> dailyRebuildTime = request.DailyRebuildTime is { } t ? Some(t) : Option<TimeSpan>.None;
|
||||
Either<BaseError, PlayoutNameViewModel> result =
|
||||
await mediator.Send(new UpdatePlayout(id, dailyRebuildTime), cancellationToken);
|
||||
|
||||
return result.Match(
|
||||
Left: error => error.ToErrorResult(),
|
||||
Right: playout => (IActionResult)new OkObjectResult(ToResponse(playout)));
|
||||
}
|
||||
|
||||
private async Task<Either<BaseError, PlayoutNameViewModel>> UpdateScheduleFile(
|
||||
PlayoutNameViewModel playout,
|
||||
string scheduleFile,
|
||||
CancellationToken cancellationToken) =>
|
||||
playout.ScheduleKind switch
|
||||
{
|
||||
PlayoutScheduleKind.Sequential => await mediator.Send(
|
||||
new UpdateSequentialPlayout(playout.PlayoutId, scheduleFile),
|
||||
cancellationToken),
|
||||
PlayoutScheduleKind.Scripted => await mediator.Send(
|
||||
new UpdateScriptedPlayout(playout.PlayoutId, scheduleFile),
|
||||
cancellationToken),
|
||||
PlayoutScheduleKind.ExternalJson => await mediator.Send(
|
||||
new UpdateExternalJsonPlayout(playout.PlayoutId, scheduleFile),
|
||||
cancellationToken),
|
||||
_ => BaseError.New("[ScheduleFile] is only valid for Sequential, Scripted, or ExternalJson playouts")
|
||||
};
|
||||
|
||||
[HttpPost("/api/playouts/reset-all", Name = "ResetAllPlayouts")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Reset all playouts")]
|
||||
|
||||
@@ -1,8 +1,31 @@
|
||||
using ErsatzTV.Application.Playouts;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api.Requests;
|
||||
|
||||
public record CreatePlayoutRequest(int ChannelId, int ProgramScheduleId)
|
||||
public record CreatePlayoutRequest(
|
||||
int ChannelId,
|
||||
PlayoutScheduleKind ScheduleKind,
|
||||
int? ProgramScheduleId,
|
||||
string ScheduleFile)
|
||||
{
|
||||
public CreateClassicPlayout ToCommand() => new(ChannelId, ProgramScheduleId);
|
||||
public Either<BaseError, CreatePlayout> ToCommand() =>
|
||||
ScheduleKind switch
|
||||
{
|
||||
PlayoutScheduleKind.Classic => ProgramScheduleId is { } programScheduleId
|
||||
? new CreateClassicPlayout(ChannelId, programScheduleId)
|
||||
: BaseError.New("[ProgramScheduleId] is required for Classic playouts"),
|
||||
PlayoutScheduleKind.Block => new CreateBlockPlayout(ChannelId),
|
||||
PlayoutScheduleKind.Sequential => !string.IsNullOrWhiteSpace(ScheduleFile)
|
||||
? new CreateSequentialPlayout(ChannelId, ScheduleFile)
|
||||
: BaseError.New("[ScheduleFile] is required for Sequential playouts"),
|
||||
PlayoutScheduleKind.Scripted => !string.IsNullOrWhiteSpace(ScheduleFile)
|
||||
? new CreateScriptedPlayout(ChannelId, ScheduleFile)
|
||||
: BaseError.New("[ScheduleFile] is required for Scripted playouts"),
|
||||
PlayoutScheduleKind.ExternalJson => !string.IsNullOrWhiteSpace(ScheduleFile)
|
||||
? new CreateExternalJsonPlayout(ChannelId, ScheduleFile)
|
||||
: BaseError.New("[ScheduleFile] is required for ExternalJson playouts"),
|
||||
_ => BaseError.New("[ScheduleKind] must be one of Classic, Block, Sequential, Scripted, ExternalJson")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Controllers.Api.Requests;
|
||||
|
||||
/// <summary>
|
||||
/// DailyRebuildTime is always applied: a null value clears the daily reset (matches the Blazor
|
||||
/// "Do not automatically reset" option in SchedulePlayoutReset.razor). ScheduleFile is only valid
|
||||
/// for Sequential, Scripted, and ExternalJson playouts; omit it (null) to leave it unchanged.
|
||||
/// </summary>
|
||||
public record UpdatePlayoutDetailsRequest(TimeSpan? DailyRebuildTime, string ScheduleFile);
|
||||
@@ -4043,7 +4043,8 @@
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Create a classic playout",
|
||||
"summary": "Create a playout",
|
||||
"description": "Creates a playout of any kind (Classic, Block, Sequential, Scripted, or ExternalJson) for a channel. Classic requires ProgramScheduleId; Sequential/Scripted/ExternalJson require ScheduleFile; Block requires neither. A channel may only have one playout.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json-patch+json": {
|
||||
@@ -4228,6 +4229,111 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Update playout scheduling details",
|
||||
"description": "DailyRebuildTime is always applied; omit it (null) to clear the daily reset. ScheduleFile is only valid for Sequential, Scripted, and ExternalJson playouts; omit it (null) to leave it unchanged.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json-patch+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdatePlayoutDetailsRequest"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdatePlayoutDetailsRequest"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdatePlayoutDetailsRequest"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdatePlayoutDetailsRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
@@ -10325,7 +10431,9 @@
|
||||
"CreatePlayoutRequest": {
|
||||
"required": [
|
||||
"channelId",
|
||||
"programScheduleId"
|
||||
"scheduleKind",
|
||||
"programScheduleId",
|
||||
"scheduleFile"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -10333,9 +10441,21 @@
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"scheduleKind": {
|
||||
"$ref": "#/components/schemas/PlayoutScheduleKind"
|
||||
},
|
||||
"programScheduleId": {
|
||||
"type": "integer",
|
||||
"type": [
|
||||
"null",
|
||||
"integer"
|
||||
],
|
||||
"format": "int32"
|
||||
},
|
||||
"scheduleFile": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13806,6 +13926,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdatePlayoutDetailsRequest": {
|
||||
"required": [
|
||||
"dailyRebuildTime",
|
||||
"scheduleFile"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dailyRebuildTime": {
|
||||
"pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$",
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
},
|
||||
"scheduleFile": {
|
||||
"type": [
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdatePlayoutSettingsRequest": {
|
||||
"required": [
|
||||
"daysToBuild",
|
||||
|
||||
Reference in New Issue
Block a user