wip: partial implementation salvaged from interrupted workflow run

Untrusted draft — no build/test had run yet. Review before building on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 21:27:40 +02:00
co-authored by Claude Fable 5
parent d1dfe6eb5a
commit 8912686a47
17 changed files with 350 additions and 1 deletions
+5 -1
View File
@@ -1,9 +1,13 @@
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Core.Api.Filler;
using ErsatzTV.Core.Domain.Filler;
namespace ErsatzTV.Application.Filler;
internal static class Mapper
{
internal static FillerPresetResponseModel ProjectToResponseModel(FillerPreset fillerPreset) =>
new(fillerPreset.Id, fillerPreset.Name);
internal static FillerPresetViewModel ProjectToViewModel(FillerPreset fillerPreset) =>
new(
fillerPreset.Id,
@@ -0,0 +1,5 @@
using ErsatzTV.Core.Api.Filler;
namespace ErsatzTV.Application.Filler;
public record GetAllFillerPresetsForApi : IRequest<List<FillerPresetResponseModel>>;
@@ -0,0 +1,22 @@
using ErsatzTV.Core.Api.Filler;
using ErsatzTV.Core.Domain.Filler;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Filler.Mapper;
namespace ErsatzTV.Application.Filler;
public class GetAllFillerPresetsForApiHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllFillerPresetsForApi, List<FillerPresetResponseModel>>
{
public async Task<List<FillerPresetResponseModel>> Handle(
GetAllFillerPresetsForApi request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<FillerPreset> fillerPresets = await dbContext.FillerPresets
.AsNoTracking()
.ToListAsync(cancellationToken);
return fillerPresets.Map(ProjectToResponseModel).ToList();
}
}
@@ -0,0 +1,5 @@
using ErsatzTV.Core.Api.Graphics;
namespace ErsatzTV.Application.Graphics;
public record GetAllGraphicsElementsForApi : IRequest<List<GraphicsElementResponseModel>>;
@@ -0,0 +1,27 @@
using ErsatzTV.Core.Api.Graphics;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Graphics.Mapper;
namespace ErsatzTV.Application.Graphics;
public class GetAllGraphicsElementsForApiHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllGraphicsElementsForApi, List<GraphicsElementResponseModel>>
{
public async Task<List<GraphicsElementResponseModel>> Handle(
GetAllGraphicsElementsForApi request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<GraphicsElement> graphicsElements = await dbContext.GraphicsElements
.AsNoTracking()
.ToListAsync(cancellationToken);
return graphicsElements
.Map(ProjectToViewModel)
.OrderBy(e => e.Name == e.FileName)
.ThenBy(e => e.Name)
.Select(vm => new GraphicsElementResponseModel(vm.Id, vm.Name))
.ToList();
}
}
@@ -1,10 +1,14 @@
using ErsatzTV.Application.Artworks;
using ErsatzTV.Core.Api.Watermarks;
using ErsatzTV.Core.Domain;
namespace ErsatzTV.Application.Watermarks;
internal static class Mapper
{
internal static WatermarkResponseModel ProjectToResponseModel(ChannelWatermark watermark) =>
new(watermark.Id, watermark.Name);
public static WatermarkViewModel ProjectToViewModel(ChannelWatermark watermark) =>
new(
watermark.Id,
@@ -0,0 +1,5 @@
using ErsatzTV.Core.Api.Watermarks;
namespace ErsatzTV.Application.Watermarks;
public record GetAllWatermarksForApi : IRequest<List<WatermarkResponseModel>>;
@@ -0,0 +1,22 @@
using ErsatzTV.Core.Api.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using static ErsatzTV.Application.Watermarks.Mapper;
namespace ErsatzTV.Application.Watermarks;
public class GetAllWatermarksForApiHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetAllWatermarksForApi, List<WatermarkResponseModel>>
{
public async Task<List<WatermarkResponseModel>> Handle(
GetAllWatermarksForApi request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
List<ChannelWatermark> watermarks = await dbContext.ChannelWatermarks
.AsNoTracking()
.ToListAsync(cancellationToken);
return watermarks.Map(ProjectToResponseModel).ToList();
}
}
@@ -0,0 +1,3 @@
namespace ErsatzTV.Core.Api.Filler;
public record FillerPresetResponseModel(int Id, string Name);
@@ -0,0 +1,3 @@
namespace ErsatzTV.Core.Api.Graphics;
public record GraphicsElementResponseModel(int Id, string Name);
@@ -0,0 +1,3 @@
namespace ErsatzTV.Core.Api.Watermarks;
public record WatermarkResponseModel(int Id, string Name);
@@ -0,0 +1,63 @@
using System.Reflection;
using ErsatzTV.Application.Filler;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Core.Api.Filler;
using MediatR;
using Microsoft.AspNetCore.Mvc.Routing;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Controllers;
[TestFixture]
public class FillerPresetControllerTests
{
private FillerPresetController _controller = null!;
private IMediator _mediator = null!;
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new FillerPresetController(_mediator);
}
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Route()
{
MethodInfo action = typeof(FillerPresetController).GetMethod(nameof(FillerPresetController.GetAll))
?? throw new AssertionException("Missing action GetAll");
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
attribute.HttpMethods.ShouldContain("GET");
attribute.Template.ShouldBe("/api/filler-presets");
}
[Test]
public async Task GetAll_Should_Return_FillerPresets()
{
List<FillerPresetResponseModel> models =
[
new FillerPresetResponseModel(1, "Intro"),
new FillerPresetResponseModel(2, "Outro")
];
_mediator.Send(Arg.Any<GetAllFillerPresetsForApi>(), Arg.Any<CancellationToken>())
.Returns(models);
List<FillerPresetResponseModel> result = await _controller.GetAll(CancellationToken.None);
result.ShouldBe(models);
}
[Test]
public async Task GetAll_Should_Return_Empty_List_When_None_Exist()
{
_mediator.Send(Arg.Any<GetAllFillerPresetsForApi>(), Arg.Any<CancellationToken>())
.Returns([]);
List<FillerPresetResponseModel> result = await _controller.GetAll(CancellationToken.None);
result.ShouldBeEmpty();
}
}
@@ -0,0 +1,63 @@
using System.Reflection;
using ErsatzTV.Application.Graphics;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Core.Api.Graphics;
using MediatR;
using Microsoft.AspNetCore.Mvc.Routing;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Controllers;
[TestFixture]
public class GraphicsElementControllerTests
{
private GraphicsElementController _controller = null!;
private IMediator _mediator = null!;
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new GraphicsElementController(_mediator);
}
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Route()
{
MethodInfo action = typeof(GraphicsElementController).GetMethod(nameof(GraphicsElementController.GetAll))
?? throw new AssertionException("Missing action GetAll");
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
attribute.HttpMethods.ShouldContain("GET");
attribute.Template.ShouldBe("/api/graphics-elements");
}
[Test]
public async Task GetAll_Should_Return_GraphicsElements()
{
List<GraphicsElementResponseModel> models =
[
new GraphicsElementResponseModel(1, "Lower Third (lower-third.png)"),
new GraphicsElementResponseModel(2, "bug.png")
];
_mediator.Send(Arg.Any<GetAllGraphicsElementsForApi>(), Arg.Any<CancellationToken>())
.Returns(models);
List<GraphicsElementResponseModel> result = await _controller.GetAll(CancellationToken.None);
result.ShouldBe(models);
}
[Test]
public async Task GetAll_Should_Return_Empty_List_When_None_Exist()
{
_mediator.Send(Arg.Any<GetAllGraphicsElementsForApi>(), Arg.Any<CancellationToken>())
.Returns([]);
List<GraphicsElementResponseModel> result = await _controller.GetAll(CancellationToken.None);
result.ShouldBeEmpty();
}
}
@@ -0,0 +1,63 @@
using System.Reflection;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Core.Api.Watermarks;
using MediatR;
using Microsoft.AspNetCore.Mvc.Routing;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
namespace ErsatzTV.Tests.Controllers;
[TestFixture]
public class WatermarkControllerTests
{
private WatermarkController _controller = null!;
private IMediator _mediator = null!;
[SetUp]
public void SetUp()
{
_mediator = Substitute.For<IMediator>();
_controller = new WatermarkController(_mediator);
}
[Test]
public void Controller_Should_Expose_Idiomatic_Rest_Route()
{
MethodInfo action = typeof(WatermarkController).GetMethod(nameof(WatermarkController.GetAll))
?? throw new AssertionException("Missing action GetAll");
HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
attribute.HttpMethods.ShouldContain("GET");
attribute.Template.ShouldBe("/api/watermarks");
}
[Test]
public async Task GetAll_Should_Return_Watermarks()
{
List<WatermarkResponseModel> models =
[
new WatermarkResponseModel(1, "Corner Logo"),
new WatermarkResponseModel(2, "Ticker")
];
_mediator.Send(Arg.Any<GetAllWatermarksForApi>(), Arg.Any<CancellationToken>())
.Returns(models);
List<WatermarkResponseModel> result = await _controller.GetAll(CancellationToken.None);
result.ShouldBe(models);
}
[Test]
public async Task GetAll_Should_Return_Empty_List_When_None_Exist()
{
_mediator.Send(Arg.Any<GetAllWatermarksForApi>(), Arg.Any<CancellationToken>())
.Returns([]);
List<WatermarkResponseModel> result = await _controller.GetAll(CancellationToken.None);
result.ShouldBeEmpty();
}
}
@@ -0,0 +1,19 @@
using ErsatzTV.Application.Filler;
using ErsatzTV.Core.Api.Filler;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers.Api;
[ApiController]
public class FillerPresetController(IMediator mediator) : ControllerBase
{
[HttpGet("/api/filler-presets")]
[Tags("Filler Presets")]
[EndpointSummary("Get all filler presets")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(List<FillerPresetResponseModel>), StatusCodes.Status200OK)]
public async Task<List<FillerPresetResponseModel>> GetAll(CancellationToken cancellationToken) =>
await mediator.Send(new GetAllFillerPresetsForApi(), cancellationToken);
}
@@ -0,0 +1,19 @@
using ErsatzTV.Application.Graphics;
using ErsatzTV.Core.Api.Graphics;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers.Api;
[ApiController]
public class GraphicsElementController(IMediator mediator) : ControllerBase
{
[HttpGet("/api/graphics-elements")]
[Tags("Graphics Elements")]
[EndpointSummary("Get all graphics elements")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(List<GraphicsElementResponseModel>), StatusCodes.Status200OK)]
public async Task<List<GraphicsElementResponseModel>> GetAll(CancellationToken cancellationToken) =>
await mediator.Send(new GetAllGraphicsElementsForApi(), cancellationToken);
}
@@ -0,0 +1,19 @@
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Api.Watermarks;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers.Api;
[ApiController]
public class WatermarkController(IMediator mediator) : ControllerBase
{
[HttpGet("/api/watermarks")]
[Tags("Watermarks")]
[EndpointSummary("Get all watermarks")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(List<WatermarkResponseModel>), StatusCodes.Status200OK)]
public async Task<List<WatermarkResponseModel>> GetAll(CancellationToken cancellationToken) =>
await mediator.Send(new GetAllWatermarksForApi(), cancellationToken);
}