feat(api): standardize ffmpeg profile endpoints
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 5m12s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m13s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped

refs #38
This commit is contained in:
2026-06-30 07:29:47 +02:00
parent 30ecd9a8a7
commit 861827dcbc
17 changed files with 1147 additions and 111 deletions
@@ -1,44 +1,106 @@
using System.ComponentModel.DataAnnotations;
using ErsatzTV.Application.FFmpegProfiles;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.FFmpegProfiles;
using ErsatzTV.Extensions;
using ErsatzTV.Filters;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers.Api;
[ApiController]
[EndpointGroupName("general")]
[ServiceFilter(typeof(ApiKeyAuthorizationFilter))]
public class FFmpegProfileController(IMediator mediator) : ControllerBase
{
[HttpGet("/api/ffmpeg/profiles", Name="GetFFmpegProfiles")]
[HttpGet("/api/ffmpeg/profiles", Name = "GetFFmpegProfiles")]
[Tags("FFmpeg Profiles")]
[EndpointSummary("Get all FFmpeg profiles")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(List<FFmpegFullProfileResponseModel>), StatusCodes.Status200OK)]
public async Task<List<FFmpegFullProfileResponseModel>> GetAll(CancellationToken cancellationToken) =>
await mediator.Send(new GetAllFFmpegProfilesForApi(), cancellationToken);
[HttpPost("/api/ffmpeg/profiles/new", Name="CreateFFmpegProfile")]
[HttpGet("/api/ffmpeg/profiles/{id:int}", Name = "GetFFmpegProfileById")]
[Tags("FFmpeg Profiles")]
[EndpointSummary("Get an FFmpeg profile by id")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(FFmpegFullProfileResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(int id, CancellationToken cancellationToken)
{
Option<FFmpegFullProfileResponseModel> result =
await mediator.Send(new GetFFmpegFullProfileByIdForApi(id), cancellationToken);
return result.ToGetResult();
}
[HttpPost("/api/ffmpeg/profiles", Name = "CreateFFmpegProfile")]
[Tags("FFmpeg Profiles")]
[EndpointSummary("Create an FFmpeg profile")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(FFmpegFullProfileResponseModel), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> AddOne(
[Required] [FromBody]
CreateFFmpegProfile request,
CreateFFmpegProfileRequest request,
CancellationToken cancellationToken)
{
Either<BaseError, CreateFFmpegProfileResult> result = await mediator.Send(request, cancellationToken);
return result.Match<IActionResult>(Ok, error => Problem(error.ToString()));
Either<BaseError, CreateFFmpegProfileResult> result = await mediator.Send(request.ToCommand(), cancellationToken);
return await result.Match(
Left: error => Task.FromResult(error.ToErrorResult()),
Right: async created =>
{
Option<FFmpegFullProfileResponseModel> profile =
await mediator.Send(new GetFFmpegFullProfileByIdForApi(created.FFmpegProfileId), cancellationToken);
return profile.Match(
Some: vm => (IActionResult)new CreatedResult($"/api/ffmpeg/profiles/{vm.Id}", vm),
None: () => ApiResults.NotFoundProblem());
});
}
[HttpPut("/api/ffmpeg/profiles/update", Name="UpdateFFmpegProfile")]
[HttpPut("/api/ffmpeg/profiles/{id:int}", Name = "UpdateFFmpegProfile")]
[Tags("FFmpeg Profiles")]
[EndpointSummary("Update an FFmpeg profile")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(FFmpegFullProfileResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> UpdateOne(
int id,
[Required] [FromBody]
UpdateFFmpegProfile request,
UpdateFFmpegProfileRequest request,
CancellationToken cancellationToken)
{
Either<BaseError, UpdateFFmpegProfileResult> result = await mediator.Send(request, cancellationToken);
return result.Match<IActionResult>(Ok, error => Problem(error.ToString()));
Either<BaseError, UpdateFFmpegProfileResult> result =
await mediator.Send(request.ToCommand(id), cancellationToken);
return await result.Match(
Left: error => Task.FromResult(error.ToErrorResult()),
Right: async updated =>
{
Option<FFmpegFullProfileResponseModel> profile =
await mediator.Send(new GetFFmpegFullProfileByIdForApi(updated.FFmpegProfileId), cancellationToken);
return profile.Match(
Some: vm => (IActionResult)new OkObjectResult(vm),
None: () => ApiResults.NotFoundProblem());
});
}
[HttpDelete("/api/ffmpeg/delete/{id:int}", Name="DeleteFFmpegProfile")]
[HttpDelete("/api/ffmpeg/profiles/{id:int}", Name = "DeleteFFmpegProfile")]
[Tags("FFmpeg Profiles")]
[EndpointSummary("Delete an FFmpeg profile")]
[EndpointGroupName("general")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> DeleteProfileAsync(int id, CancellationToken cancellationToken)
{
Either<BaseError, Unit> result = await mediator.Send(new DeleteFFmpegProfile(id), cancellationToken);
return result.Match<IActionResult>(_ => Ok(), error => Conflict(error.ToString()));
return result.ToDeletedResult();
}
}
@@ -1,5 +1,6 @@
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Filters;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -7,6 +8,7 @@ namespace ErsatzTV.Controllers.Api;
[ApiController]
[EndpointGroupName("general")]
[ServiceFilter(typeof(ApiKeyAuthorizationFilter))]
public class LibrariesController(ITelevisionRepository televisionRepository, IMediator mediator)
{
[HttpPost("/api/libraries/{id:int}/scan")]
@@ -2,6 +2,7 @@ using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Maintenance;
using ErsatzTV.Core;
using ErsatzTV.Filters;
using MediatR;
using Microsoft.AspNetCore.Mvc;
@@ -9,6 +10,7 @@ namespace ErsatzTV.Controllers.Api;
[ApiController]
[EndpointGroupName("general")]
[ServiceFilter(typeof(ApiKeyAuthorizationFilter))]
public class MaintenanceController(IMediator mediator, ChannelWriter<IBackgroundServiceRequest> workerChannel)
{
[HttpGet("/api/maintenance/gc")]
@@ -0,0 +1,71 @@
using ErsatzTV.Application.FFmpegProfiles;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
namespace ErsatzTV.Controllers.Api.Requests;
public record CreateFFmpegProfileRequest(
string Name,
int ThreadCount,
bool NormalizeAudio,
bool NormalizeVideo,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FilterMode PadMode,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
NormalizeLoudnessMode NormalizeLoudnessMode,
double? TargetLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
bool NormalizeColors,
bool DeinterlaceVideo)
{
public CreateFFmpegProfile ToCommand() =>
new(
Name,
ThreadCount,
NormalizeAudio,
NormalizeVideo,
HardwareAcceleration,
VaapiDisplay,
VaapiDriver,
VaapiDevice,
QsvExtraHardwareFrames,
ResolutionId,
ScalingBehavior,
PadMode,
VideoFormat,
VideoProfile,
VideoPreset,
AllowBFrames,
BitDepth,
VideoBitrate,
VideoBufferSize,
TonemapAlgorithm,
AudioFormat,
AudioBitrate,
AudioBufferSize,
NormalizeLoudnessMode,
TargetLoudness,
AudioChannels,
AudioSampleRate,
NormalizeFramerate,
NormalizeColors,
DeinterlaceVideo);
}
@@ -0,0 +1,72 @@
using ErsatzTV.Application.FFmpegProfiles;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.FFmpeg;
namespace ErsatzTV.Controllers.Api.Requests;
public record UpdateFFmpegProfileRequest(
string Name,
int ThreadCount,
bool NormalizeAudio,
bool NormalizeVideo,
HardwareAccelerationKind HardwareAcceleration,
string VaapiDisplay,
VaapiDriver VaapiDriver,
string VaapiDevice,
int? QsvExtraHardwareFrames,
int ResolutionId,
ScalingBehavior ScalingBehavior,
FilterMode PadMode,
FFmpegProfileVideoFormat VideoFormat,
string VideoProfile,
string VideoPreset,
bool AllowBFrames,
FFmpegProfileBitDepth BitDepth,
int VideoBitrate,
int VideoBufferSize,
FFmpegProfileTonemapAlgorithm TonemapAlgorithm,
FFmpegProfileAudioFormat AudioFormat,
int AudioBitrate,
int AudioBufferSize,
NormalizeLoudnessMode NormalizeLoudnessMode,
double? TargetLoudness,
int AudioChannels,
int AudioSampleRate,
bool NormalizeFramerate,
bool NormalizeColors,
bool DeinterlaceVideo)
{
public UpdateFFmpegProfile ToCommand(int id) =>
new(
id,
Name,
ThreadCount,
NormalizeAudio,
NormalizeVideo,
HardwareAcceleration,
VaapiDisplay,
VaapiDriver,
VaapiDevice,
QsvExtraHardwareFrames,
ResolutionId,
ScalingBehavior,
PadMode,
VideoFormat,
VideoProfile,
VideoPreset,
AllowBFrames,
BitDepth,
VideoBitrate,
VideoBufferSize,
TonemapAlgorithm,
AudioFormat,
AudioBitrate,
AudioBufferSize,
NormalizeLoudnessMode,
TargetLoudness,
AudioChannels,
AudioSampleRate,
NormalizeFramerate,
NormalizeColors,
DeinterlaceVideo);
}