using System.Diagnostics; using System.IO; using System.Text; namespace EmbyToolbox.Services; public sealed class FfprobeService { public async Task AnalyzeAsync(string filePath, CancellationToken cancellationToken = default) => await AnalyzeInternalAsync(filePath, "-v error -show_format -show_streams -show_chapters -print_format json", cancellationToken); public async Task AnalyzeSubtitlePacketsAsync(string filePath, CancellationToken cancellationToken = default) => await AnalyzeInternalAsync( filePath, "-v error -select_streams s -show_packets -show_entries packet=stream_index,duration_time -print_format json", cancellationToken); private async Task AnalyzeInternalAsync(string filePath, string argumentPrefix, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath)) { return FfprobeResult.Fail("Файл не выбран или не существует."); } var ffprobePath = ResolveFfprobePath(); if (!File.Exists(ffprobePath)) { return FfprobeResult.Fail($"ffprobe не найден: {ffprobePath}"); } var arguments = $"{argumentPrefix} \"{filePath}\""; var startInfo = new ProcessStartInfo { FileName = ffprobePath, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; using var process = new Process { StartInfo = startInfo }; process.Start(); using var killRegistration = cancellationToken.Register( static state => { try { if (state is not Process p || p.HasExited) { return; } p.Kill(entireProcessTree: true); } catch { // ignore } }, process, useSynchronizationContext: false); var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); try { await process.WaitForExitAsync(cancellationToken); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { try { await Task.WhenAll(outputTask, errorTask); } catch { // ignore } throw; } var output = await outputTask; var error = await errorTask; if (process.ExitCode != 0) { var message = string.IsNullOrWhiteSpace(error) ? $"ffprobe завершился с кодом {process.ExitCode}." : error.Trim(); return FfprobeResult.Fail(message, $"{ffprobePath} {arguments}", output, error); } if (string.IsNullOrWhiteSpace(output)) { return FfprobeResult.Fail("ffprobe вернул пустой JSON-результат.", $"{ffprobePath} {arguments}", output, error); } return FfprobeResult.Ok(output, $"{ffprobePath} {arguments}", output, error); } private static string ResolveFfprobePath() { return Path.Combine(AppContext.BaseDirectory, "Tools", "ffprobe.exe"); } } public sealed class FfprobeResult { private FfprobeResult(bool isSuccess, string json, string error, string command, string stdOut, string stdErr) { IsSuccess = isSuccess; Json = json; Error = error; Command = command; StdOut = stdOut; StdErr = stdErr; } public bool IsSuccess { get; } public string Json { get; } public string Error { get; } public string Command { get; } public string StdOut { get; } public string StdErr { get; } public static FfprobeResult Ok(string json, string command, string stdOut, string stdErr) { return new FfprobeResult(true, json, string.Empty, command, stdOut, stdErr); } public static FfprobeResult Fail(string error, string command = "", string stdOut = "", string stdErr = "") { return new FfprobeResult(false, string.Empty, error, command, stdOut, stdErr); } }