using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Runtime; using LanguageExt; namespace ErsatzTV.Application.FFmpegProfiles.Commands { public class UpdateFFmpegSettingsHandler : MediatR.IRequestHandler> { private readonly IConfigElementRepository _configElementRepository; private readonly ILocalFileSystem _localFileSystem; private readonly IRuntimeInfo _runtimeInfo; public UpdateFFmpegSettingsHandler( IConfigElementRepository configElementRepository, ILocalFileSystem localFileSystem, IRuntimeInfo runtimeInfo) { _configElementRepository = configElementRepository; _localFileSystem = localFileSystem; _runtimeInfo = runtimeInfo; } public Task> Handle( UpdateFFmpegSettings request, CancellationToken cancellationToken) => Validate(request) .MapT(_ => ApplyUpdate(request)) .Bind(v => v.ToEitherAsync()); private async Task> Validate(UpdateFFmpegSettings request) => (await FFmpegMustExist(request), await FFprobeMustExist(request), ReportsAreNotSupportedOnWindows(request)) .Apply((_, _, _) => Unit.Default); private Task> FFmpegMustExist(UpdateFFmpegSettings request) => ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg"); private Task> FFprobeMustExist(UpdateFFmpegSettings request) => ValidateToolPath(request.Settings.FFprobePath, "ffprobe"); private Validation ReportsAreNotSupportedOnWindows(UpdateFFmpegSettings request) { if (request.Settings.SaveReports && _runtimeInfo.IsOSPlatform(OSPlatform.Windows)) { return BaseError.New("FFmpeg reports are not supported on Windows"); } return Unit.Default; } private async Task> ValidateToolPath(string path, string name) { if (!_localFileSystem.FileExists(path)) { return BaseError.New($"{name} path does not exist"); } var startInfo = new ProcessStartInfo { FileName = path, Arguments = "-version", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; var test = new Process { StartInfo = startInfo }; test.Start(); string output = await test.StandardOutput.ReadToEndAsync(); await test.WaitForExitAsync(); return test.ExitCode == 0 && output.Contains($"{name} version") ? Unit.Default : BaseError.New($"Unable to verify {name} version"); } private async Task ApplyUpdate(UpdateFFmpegSettings request) { await _configElementRepository.Upsert(ConfigElementKey.FFmpegPath, request.Settings.FFmpegPath); await _configElementRepository.Upsert(ConfigElementKey.FFprobePath, request.Settings.FFprobePath); await _configElementRepository.Upsert( ConfigElementKey.FFmpegDefaultProfileId, request.Settings.DefaultFFmpegProfileId.ToString()); await _configElementRepository.Upsert( ConfigElementKey.FFmpegSaveReports, request.Settings.SaveReports.ToString()); if (request.Settings.SaveReports && !Directory.Exists(FileSystemLayout.FFmpegReportsFolder)) { Directory.CreateDirectory(FileSystemLayout.FFmpegReportsFolder); } await _configElementRepository.Upsert( ConfigElementKey.FFmpegPreferredLanguageCode, request.Settings.PreferredLanguageCode); if (request.Settings.GlobalWatermarkId is not null) { await _configElementRepository.Upsert( ConfigElementKey.FFmpegGlobalWatermarkId, request.Settings.GlobalWatermarkId.Value); } else { await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalWatermarkId); } if (request.Settings.GlobalFallbackFillerId is not null) { await _configElementRepository.Upsert( ConfigElementKey.FFmpegGlobalFallbackFillerId, request.Settings.GlobalFallbackFillerId.Value); } else { await _configElementRepository.Delete(ConfigElementKey.FFmpegGlobalFallbackFillerId); } await _configElementRepository.Upsert( ConfigElementKey.FFmpegSegmenterTimeout, request.Settings.HlsSegmenterIdleTimeout); await _configElementRepository.Upsert( ConfigElementKey.FFmpegWorkAheadSegmenters, request.Settings.WorkAheadSegmenterLimit); return Unit.Default; } } }