using System.ComponentModel.DataAnnotations; using ErsatzTV.Application.Scheduling; using ErsatzTV.Controllers.Api.Requests; using ErsatzTV.Core; using ErsatzTV.Core.Api.Scheduling; using ErsatzTV.Extensions; using MediatR; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace ErsatzTV.Controllers.Api; [ApiController] public class DecoController(IMediator mediator) : ControllerBase { [HttpGet("/api/v1/decos/groups", Name = "GetDecoGroups")] [Tags("Decos")] [EndpointSummary("Get all deco groups")] [EndpointGroupName("general")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task> GetGroups(CancellationToken cancellationToken) { List groups = await mediator.Send(new GetAllDecoGroups(), cancellationToken); return groups.Map(ProjectToResponseModel).ToList(); } [HttpPost("/api/v1/decos/groups")] [Tags("Decos")] [EndpointSummary("Create a deco group")] [EndpointGroupName("general")] [ProducesResponseType(typeof(DecoGroupResponseModel), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] public async Task CreateGroup( [Required][FromBody] CreateDecoGroupRequest request, CancellationToken cancellationToken) { Either result = await mediator.Send(request.ToCommand(), cancellationToken); return result.ToCreatedResult( vm => $"/api/v1/decos/groups/{vm.Id}", vm => ProjectToResponseModel(vm)); } [HttpDelete("/api/v1/decos/groups/{id:int}")] [Tags("Decos")] [EndpointSummary("Delete a deco group")] [EndpointDescription( "Deletes the deco group. The database cascade removes every deco in the group (and each deco's break " + "content and watermark/graphics-element assignments); any playout that used a removed deco has its " + "default deco cleared.")] [EndpointGroupName("general")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task DeleteGroup(int id, CancellationToken cancellationToken) { List groups = await mediator.Send(new GetAllDecoGroups(), cancellationToken); if (groups.All(g => g.Id != id)) { return ApiResults.NotFoundProblem(); } Option result = await mediator.Send(new DeleteDecoGroup(id), cancellationToken); return result.Match( Some: error => error.ToErrorResult(), None: () => new NoContentResult()); } [HttpGet("/api/v1/decos")] [Tags("Decos")] [EndpointSummary("Get all decos")] [EndpointDescription("Returns every deco as a flat list, ordered by group name then deco name.")] [EndpointGroupName("general")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public async Task> GetAll(CancellationToken cancellationToken) { List groups = await mediator.Send(new GetAllDecoGroups(), cancellationToken); var result = new List(); foreach (DecoGroupViewModel group in groups) { List decos = await mediator.Send(new GetDecosByDecoGroupId(group.Id), cancellationToken); result.AddRange(decos.Map(d => new DecoListItemResponseModel(d.Id, d.DecoGroupId, d.DecoGroupName, d.Name))); } return result; } [HttpGet("/api/v1/decos/{id:int}", Name = "GetDecoById")] [Tags("Decos")] [EndpointSummary("Get a deco by id")] [EndpointGroupName("general")] [ProducesResponseType(typeof(DecoResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task GetById(int id, CancellationToken cancellationToken) { Option result = await mediator.Send(new GetDecoById(id), cancellationToken); return result.Map(ProjectToResponseModel).ToGetResult(); } [HttpPost("/api/v1/decos")] [Tags("Decos")] [EndpointSummary("Create a deco")] [EndpointDescription("Creates a deco in the given deco group. The deco inherits everything by default.")] [EndpointGroupName("general")] [ProducesResponseType(typeof(DecoResponseModel), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] public async Task Create( [Required][FromBody] CreateDecoRequest request, CancellationToken cancellationToken) { Either result = await mediator.Send(request.ToCommand(), cancellationToken); return result.ToCreatedResult( vm => $"/api/v1/decos/{vm.Id}", vm => ProjectToResponseModel(vm)); } [HttpDelete("/api/v1/decos/{id:int}")] [Tags("Decos")] [EndpointSummary("Delete a deco")] [EndpointDescription( "Deletes the deco. Any playout that used it as its default deco has that default cleared.")] [EndpointGroupName("general")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] public async Task Delete(int id, CancellationToken cancellationToken) { Option existing = await mediator.Send(new GetDecoById(id), cancellationToken); if (existing.IsNone) { return ApiResults.NotFoundProblem(); } Option result = await mediator.Send(new DeleteDeco(id), cancellationToken); return result.Match( Some: error => error.ToErrorResult(), None: () => new NoContentResult()); } [HttpPut("/api/v1/decos/{id:int}")] [Tags("Decos")] [EndpointSummary("Replace a deco")] [EndpointDescription( "Replaces the full state of the deco (name and all six sections). Send the entire desired state, including " + "unchanged sections. Merge is only valid for the watermark and graphics-element modes. Break content allows " + "at most one item per placement.")] [EndpointGroupName("general")] [ProducesResponseType(typeof(DecoResponseModel), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)] public async Task Replace( int id, [Required][FromBody] ReplaceDecoRequest request, CancellationToken cancellationToken) { Option maybeDeco = await mediator.Send(new GetDecoById(id), cancellationToken); if (maybeDeco.IsNone) { return ApiResults.NotFoundProblem(); } int decoGroupId = maybeDeco.Map(d => d.DecoGroupId).IfNone(0); Either commandOrError = request.ToCommand(decoGroupId, id); return await commandOrError.Match( Left: error => Task.FromResult(error.ToErrorResult()), Right: async command => { Either result = await mediator.Send(command, cancellationToken); return await result.Match( Left: error => Task.FromResult(error.ToErrorResult()), Right: async _ => { Option refreshed = await mediator.Send(new GetDecoById(id), cancellationToken); return refreshed.Match( Some: vm => (IActionResult)new OkObjectResult(ProjectToResponseModel(vm)), None: () => ApiResults.NotFoundProblem()); }); }); } private static DecoGroupResponseModel ProjectToResponseModel(DecoGroupViewModel vm) => new(vm.Id, vm.Name, vm.DecoCount); private static DecoResponseModel ProjectToResponseModel(DecoViewModel vm) => new( vm.Id, vm.DecoGroupId, vm.DecoGroupName, vm.Name, vm.WatermarkMode, vm.Watermarks.Map(w => w.Id).ToList(), vm.UseWatermarkDuringFiller, vm.GraphicsElementsMode, vm.GraphicsElements.Map(g => g.Id).ToList(), vm.UseGraphicsElementsDuringFiller, vm.BreakContentMode, vm.BreakContent.Map(ProjectToResponseModel).ToList(), vm.DefaultFillerMode, vm.DefaultFillerCollectionType, vm.DefaultFillerCollection?.Id, vm.DefaultFillerMediaItem?.MediaItemId, vm.DefaultFillerMultiCollection?.Id, vm.DefaultFillerSmartCollection?.Id, vm.DefaultFillerCollection?.Name ?? vm.DefaultFillerMediaItem?.Name ?? vm.DefaultFillerMultiCollection?.Name ?? vm.DefaultFillerSmartCollection?.Name, vm.DefaultFillerTrimToFit, vm.DeadAirFallbackMode, vm.DeadAirFallbackCollectionType, vm.DeadAirFallbackCollection?.Id, vm.DeadAirFallbackMediaItem?.MediaItemId, vm.DeadAirFallbackMultiCollection?.Id, vm.DeadAirFallbackSmartCollection?.Id, vm.DeadAirFallbackCollection?.Name ?? vm.DeadAirFallbackMediaItem?.Name ?? vm.DeadAirFallbackMultiCollection?.Name ?? vm.DeadAirFallbackSmartCollection?.Name); private static DecoBreakContentResponseModel ProjectToResponseModel(DecoBreakContentViewModel vm) => new( vm.Id, vm.CollectionType, vm.Collection?.Id, vm.MediaItem?.MediaItemId, vm.MultiCollection?.Id, vm.SmartCollection?.Id, vm.Playlist?.Id, vm.Playlist?.PlaylistGroupId, vm.Collection?.Name ?? vm.MediaItem?.Name ?? vm.MultiCollection?.Name ?? vm.SmartCollection?.Name ?? vm.Playlist?.Name, vm.Placement); }