Backend slice for the ChicoryTV playouts and collections screens.
PlayoutController:
- POST /api/playouts/{id}/erase-items (204; 404 pre-check; 422 unless
Block/Sequential/Scripted) -> ErasePlayoutItems
- POST /api/playouts/{id}/erase-items-and-history (204; 404; 422 unless
Classic/Block/Sequential/Scripted) -> ErasePlayoutHistory
- GET /api/playouts/items/{id}/scheduling-context (200/404) decodes a
playout item's stored context by row id via a new
GetPlayoutItemSchedulingContext query that reuses ProcessSchedulingContext
- PlayoutItemResponseModel gains HasSchedulingContext (no raw JSON in list)
- PlayoutListItemResponseModel gains PlayoutMode (ChannelNumber already present)
CollectionController:
- PUT /api/collections/{id}/custom-order (204; 404 pre-check; 422) with
UpdateCollectionCustomOrderRequest deriving CustomIndex from array order
- GetCollectionItemsHandler orders by CustomIndex (nulls last) then title/id
when the collection's UseCustomPlaybackOrder is set
Tests: controller route + behavior tests, OpenAPI ProblemDetails TestCases,
GetCollectionItems custom-order handler test. Regenerated v1.json, v1.d.ts,
endpoint-index.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,33 @@ public class CollectionController(IMediator mediator) : ControllerBase
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/api/collections/{id:int}/custom-order")]
|
||||
[Tags("Collections")]
|
||||
[EndpointSummary("Set a collection's custom playback order")]
|
||||
[EndpointDescription(
|
||||
"Replaces the custom playback order of a manual collection. CustomIndex is assigned from array position " +
|
||||
"(the request body has no index field); ids that are not members of the collection are ignored. Set " +
|
||||
"UseCustomPlaybackOrder on the collection for this order to take effect.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<IActionResult> UpdateCustomOrder(
|
||||
int id,
|
||||
[Required] [FromBody] UpdateCollectionCustomOrderRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<MediaCollectionViewModel> maybeCollection =
|
||||
await mediator.Send(new GetCollectionById(id), cancellationToken);
|
||||
if (maybeCollection.IsNone)
|
||||
{
|
||||
return ApiResults.NotFoundProblem();
|
||||
}
|
||||
|
||||
Either<BaseError, Unit> result = await mediator.Send(request.ToCommand(id), cancellationToken);
|
||||
return result.ToDeletedResult();
|
||||
}
|
||||
|
||||
[HttpDelete("/api/collections/{id:int}")]
|
||||
[Tags("Collections")]
|
||||
[EndpointSummary("Delete a collection")]
|
||||
|
||||
@@ -521,6 +521,87 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
return Accepted();
|
||||
}
|
||||
|
||||
[HttpPost("/api/playouts/{id:int}/erase-items", Name = "ErasePlayoutItems")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Erase a playout's items")]
|
||||
[EndpointDescription(
|
||||
"Deletes the built items (plus gaps and build status) for a Block, Sequential, or Scripted playout, " +
|
||||
"preserving history that precedes the currently-airing item. Only valid for those kinds; other kinds " +
|
||||
"return 422.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<IActionResult> EraseItems(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<PlayoutNameViewModel> 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 or PlayoutScheduleKind.Sequential
|
||||
or PlayoutScheduleKind.Scripted))
|
||||
{
|
||||
return BaseError.New("[EraseItems] is only valid for Block, Sequential, or Scripted playouts")
|
||||
.ToErrorResult();
|
||||
}
|
||||
}
|
||||
|
||||
await mediator.Send(new ErasePlayoutItems(id), cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("/api/playouts/{id:int}/erase-items-and-history", Name = "ErasePlayoutItemsAndHistory")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Erase a playout's items and history")]
|
||||
[EndpointDescription(
|
||||
"Deletes all built items, history, anchors, and build status for a Classic, Block, Sequential, or " +
|
||||
"Scripted playout, and reseeds it. Only valid for those kinds; other kinds return 422.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
|
||||
public async Task<IActionResult> EraseItemsAndHistory(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<PlayoutNameViewModel> 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 or PlayoutScheduleKind.Block
|
||||
or PlayoutScheduleKind.Sequential or PlayoutScheduleKind.Scripted))
|
||||
{
|
||||
return BaseError.New(
|
||||
"[EraseItemsAndHistory] is only valid for Classic, Block, Sequential, or Scripted playouts")
|
||||
.ToErrorResult();
|
||||
}
|
||||
}
|
||||
|
||||
await mediator.Send(new ErasePlayoutHistory(id), cancellationToken);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("/api/playouts/items/{id:int}/scheduling-context", Name = "GetPlayoutItemSchedulingContext")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Decode a playout item's scheduling context")]
|
||||
[EndpointDescription(
|
||||
"Decodes the stored scheduling context for a single playout item (by its row id) into readable, enriched " +
|
||||
"JSON. Returns 404 when the item is missing or has no scheduling context.")]
|
||||
[EndpointGroupName("general")]
|
||||
[ProducesResponseType(typeof(PlayoutItemSchedulingContextResponseModel), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> GetItemSchedulingContext(int id, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<string> result = await mediator.Send(new GetPlayoutItemSchedulingContext(id), cancellationToken);
|
||||
return result.Map(context => new PlayoutItemSchedulingContextResponseModel(context)).ToGetResult();
|
||||
}
|
||||
|
||||
[HttpDelete("/api/playouts/{id:int}")]
|
||||
[Tags("Playouts")]
|
||||
[EndpointSummary("Delete a playout")]
|
||||
@@ -649,7 +730,8 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
vm.ScheduleKind,
|
||||
vm.ScheduleName,
|
||||
vm.DbDailyRebuildTime,
|
||||
ToBuildStatus(vm.BuildStatus));
|
||||
ToBuildStatus(vm.BuildStatus),
|
||||
vm.PlayoutMode);
|
||||
|
||||
private static PlayoutBuildStatusResponseModel ToBuildStatus(PlayoutBuildStatus buildStatus) =>
|
||||
buildStatus is null
|
||||
@@ -665,5 +747,6 @@ public class PlayoutController(IMediator mediator) : ControllerBase
|
||||
vm.Start,
|
||||
vm.Finish,
|
||||
vm.Duration,
|
||||
vm.FillerKind.MatchUnsafe(fk => (FillerKind?)fk, () => null));
|
||||
vm.FillerKind.MatchUnsafe(fk => (FillerKind?)fk, () => null),
|
||||
!string.IsNullOrWhiteSpace(vm.SchedulingContext));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using ErsatzTV.Application.MediaCollections;
|
||||
|
||||
namespace ErsatzTV.Controllers.Api.Requests;
|
||||
|
||||
public record UpdateCollectionCustomOrderRequest(List<int> MediaItemIds)
|
||||
{
|
||||
public UpdateCollectionCustomOrder ToCommand(int collectionId) =>
|
||||
new(
|
||||
collectionId,
|
||||
(MediaItemIds ?? [])
|
||||
.Select((mediaItemId, index) => new MediaItemCustomOrder(mediaItemId, index))
|
||||
.ToList());
|
||||
}
|
||||
@@ -2793,6 +2793,96 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/collections/{id}/custom-order": {
|
||||
"put": {
|
||||
"tags": [
|
||||
"Collections"
|
||||
],
|
||||
"summary": "Set a collection's custom playback order",
|
||||
"description": "Replaces the custom playback order of a manual collection. CustomIndex is assigned from array position (the request body has no index field); ids that are not members of the collection are ignored. Set UseCustomPlaybackOrder on the collection for this order to take effect.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json-patch+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateCollectionCustomOrderRequest"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateCollectionCustomOrderRequest"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateCollectionCustomOrderRequest"
|
||||
}
|
||||
},
|
||||
"application/*+json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateCollectionCustomOrderRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
},
|
||||
"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/collections/{id}/items/{mediaItemId}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
@@ -8132,6 +8222,201 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/playouts/{id}/erase-items": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Erase a playout's items",
|
||||
"description": "Deletes the built items (plus gaps and build status) for a Block, Sequential, or Scripted playout, preserving history that precedes the currently-airing item. Only valid for those kinds; other kinds return 422.",
|
||||
"operationId": "ErasePlayoutItems",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
},
|
||||
"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}/erase-items-and-history": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Erase a playout's items and history",
|
||||
"description": "Deletes all built items, history, anchors, and build status for a Classic, Block, Sequential, or Scripted playout, and reseeds it. Only valid for those kinds; other kinds return 422.",
|
||||
"operationId": "ErasePlayoutItemsAndHistory",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
},
|
||||
"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/items/{id}/scheduling-context": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Playouts"
|
||||
],
|
||||
"summary": "Decode a playout item's scheduling context",
|
||||
"description": "Decodes the stored scheduling context for a single playout item (by its row id) into readable, enriched JSON. Returns 404 when the item is missing or has no scheduling context.",
|
||||
"operationId": "GetPlayoutItemSchedulingContext",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"text/plain": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutItemSchedulingContextResponseModel"
|
||||
}
|
||||
},
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutItemSchedulingContextResponseModel"
|
||||
}
|
||||
},
|
||||
"text/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PlayoutItemSchedulingContextResponseModel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/rerun-collections": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -19053,7 +19338,8 @@
|
||||
"start",
|
||||
"finish",
|
||||
"duration",
|
||||
"fillerKind"
|
||||
"fillerKind",
|
||||
"hasSchedulingContext"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19086,6 +19372,20 @@
|
||||
"$ref": "#/components/schemas/FillerKind"
|
||||
}
|
||||
]
|
||||
},
|
||||
"hasSchedulingContext": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PlayoutItemSchedulingContextResponseModel": {
|
||||
"required": [
|
||||
"context"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -19097,7 +19397,8 @@
|
||||
"scheduleKind",
|
||||
"scheduleName",
|
||||
"dailyRebuildTime",
|
||||
"buildStatus"
|
||||
"buildStatus",
|
||||
"playoutMode"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -19133,6 +19434,9 @@
|
||||
"$ref": "#/components/schemas/PlayoutBuildStatusResponseModel"
|
||||
}
|
||||
]
|
||||
},
|
||||
"playoutMode": {
|
||||
"$ref": "#/components/schemas/ChannelPlayoutMode"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -21394,6 +21698,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateCollectionCustomOrderRequest": {
|
||||
"required": [
|
||||
"mediaItemIds"
|
||||
],
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mediaItemIds": {
|
||||
"type": [
|
||||
"null",
|
||||
"array"
|
||||
],
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdateCollectionRequest": {
|
||||
"required": [
|
||||
"name",
|
||||
|
||||
Reference in New Issue
Block a user