Files
ersatztv/ErsatzTV.Application/FFmpegProfiles/Commands/CreateFFmpegProfileHandler.cs
T
timothy 861827dcbc
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
feat(api): standardize ffmpeg profile endpoints
refs #38
2026-06-30 07:29:47 +02:00

127 lines
5.4 KiB
C#

using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.FFmpegProfiles;
public class CreateFFmpegProfileHandler :
IRequestHandler<CreateFFmpegProfile, Either<BaseError, CreateFFmpegProfileResult>>
{
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly ISearchTargets _searchTargets;
public CreateFFmpegProfileHandler(IDbContextFactory<TvContext> dbContextFactory, ISearchTargets searchTargets)
{
_dbContextFactory = dbContextFactory;
_searchTargets = searchTargets;
}
public async Task<Either<BaseError, CreateFFmpegProfileResult>> Handle(
CreateFFmpegProfile request,
CancellationToken cancellationToken)
{
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
Option<int> maybeResolutionId = await ResolutionMustExist(dbContext, request, cancellationToken);
return await maybeResolutionId.Match(
Some: async resolutionId =>
{
Validation<BaseError, FFmpegProfile> validation = Validate(request, resolutionId);
return await validation.Apply(profile => PersistFFmpegProfile(dbContext, profile));
},
None: () => Task.FromResult<Either<BaseError, CreateFFmpegProfileResult>>(
new NotFoundError($"[Resolution] {request.ResolutionId} does not exist")));
}
private async Task<CreateFFmpegProfileResult> PersistFFmpegProfile(
TvContext dbContext,
FFmpegProfile ffmpegProfile)
{
await dbContext.FFmpegProfiles.AddAsync(ffmpegProfile);
await dbContext.SaveChangesAsync();
_searchTargets.SearchTargetsChanged();
return new CreateFFmpegProfileResult(ffmpegProfile.Id);
}
private static Validation<BaseError, FFmpegProfile> Validate(
CreateFFmpegProfile request,
int resolutionId) =>
(ValidateName(request), ValidateThreadCount(request))
.Apply((name, threadCount) =>
{
var hwAccel = request.NormalizeVideo
? request.HardwareAcceleration
: HardwareAccelerationKind.None;
return new FFmpegProfile
{
Name = name,
ThreadCount = threadCount,
NormalizeAudio = request.NormalizeAudio,
NormalizeVideo = request.NormalizeVideo,
HardwareAcceleration = hwAccel,
VaapiDriver = request.VaapiDriver,
VaapiDevice = request.VaapiDevice,
QsvExtraHardwareFrames = request.QsvExtraHardwareFrames,
ResolutionId = resolutionId,
ScalingBehavior = request.ScalingBehavior,
// only allow customization with VAAPI accel
PadMode = hwAccel switch
{
HardwareAccelerationKind.None => FilterMode.Software,
HardwareAccelerationKind.Vaapi => request.PadMode,
_ => FilterMode.HardwareIfPossible
},
VideoFormat = request.NormalizeVideo ? request.VideoFormat : FFmpegProfileVideoFormat.Copy,
VideoProfile = request.VideoProfile,
VideoPreset = request.VideoPreset,
AllowBFrames = request.AllowBFrames,
// mpeg2video only supports 8-bit content
BitDepth = request.VideoFormat is FFmpegProfileVideoFormat.Mpeg2Video
? FFmpegProfileBitDepth.EightBit
: request.BitDepth,
VideoBitrate = request.VideoBitrate,
VideoBufferSize = request.VideoBufferSize,
TonemapAlgorithm = request.TonemapAlgorithm,
AudioFormat = request.NormalizeAudio ? request.AudioFormat : FFmpegProfileAudioFormat.Copy,
AudioBitrate = request.AudioBitrate,
AudioBufferSize = request.AudioBufferSize,
NormalizeLoudnessMode = request.NormalizeLoudnessMode,
TargetLoudness = request.NormalizeLoudnessMode is NormalizeLoudnessMode.LoudNorm
? request.TargetLoudness
: null,
AudioChannels = request.AudioChannels,
AudioSampleRate = request.AudioSampleRate,
NormalizeFramerate = request.NormalizeFramerate,
NormalizeColors = request.NormalizeColors,
DeinterlaceVideo = request.DeinterlaceVideo
};
});
private static Validation<BaseError, string> ValidateName(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.NotEmpty(x => x.Name)
.Bind(_ => createFFmpegProfile.NotLongerThan(50)(x => x.Name));
private static Validation<BaseError, int> ValidateThreadCount(CreateFFmpegProfile createFFmpegProfile) =>
createFFmpegProfile.AtLeast(0)(p => p.ThreadCount);
private static Task<Option<int>> ResolutionMustExist(
TvContext dbContext,
CreateFFmpegProfile createFFmpegProfile,
CancellationToken cancellationToken) =>
dbContext.Resolutions
.SelectOneAsync(r => r.Id, r => r.Id == createFFmpegProfile.ResolutionId, cancellationToken)
.MapT(r => r.Id);
}