111 lines
3.3 KiB
C#
111 lines
3.3 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Text;
|
|
using EmbyToolbox.Models;
|
|
|
|
namespace EmbyToolbox.Services;
|
|
|
|
public sealed class TrackExtractionService
|
|
{
|
|
private readonly FfprobeService _ffprobe;
|
|
|
|
public TrackExtractionService(FfprobeService ffprobe)
|
|
{
|
|
_ffprobe = ffprobe;
|
|
}
|
|
|
|
public async Task<MediaAnalysisResult?> AnalyzeMediaAsync(string filePath, LoggingService logging, CancellationToken ct)
|
|
{
|
|
var probe = await _ffprobe.AnalyzeAsync(filePath, ct).ConfigureAwait(false);
|
|
if (!probe.IsSuccess)
|
|
{
|
|
logging.Error($"ffprobe: {probe.Error}", "tracks.extract");
|
|
return null;
|
|
}
|
|
|
|
var parsed = MediaAnalysisParser.TryParse(probe.Json);
|
|
if (parsed is null)
|
|
{
|
|
logging.Error($"ffprobe JSON не распознан: {Path.GetFileName(filePath)}", "tracks.extract");
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
/// <summary>Создаёт <c>[destination]\extract\{audio|subtitles|attachments}</c>, возвращает путь к <c>extract</c>.</summary>
|
|
public string? PrepareExtractLayout(string destinationRootFolder) =>
|
|
TrackExtractOutputPaths.TryPrepareExtractDirectories(destinationRootFolder);
|
|
|
|
/// <returns>Успех, stderr ffmpeg.</returns>
|
|
public async Task<(bool Success, string StdErr)> RunExtractProcessAsync(IReadOnlyList<string> ffmpegArgumentList, CancellationToken ct)
|
|
{
|
|
var exe = ResolveFfmpegPath();
|
|
if (!File.Exists(exe))
|
|
{
|
|
return (false, $"ffmpeg не найден: {exe}");
|
|
}
|
|
|
|
var start = new ProcessStartInfo
|
|
{
|
|
FileName = exe,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
StandardOutputEncoding = Encoding.UTF8,
|
|
StandardErrorEncoding = Encoding.UTF8,
|
|
};
|
|
|
|
foreach (var a in ffmpegArgumentList)
|
|
{
|
|
start.ArgumentList.Add(a);
|
|
}
|
|
|
|
using var p = new Process { StartInfo = start };
|
|
p.Start();
|
|
using var reg = ct.Register(static state =>
|
|
{
|
|
try
|
|
{
|
|
if (state is not Process proc || proc.HasExited)
|
|
{
|
|
return;
|
|
}
|
|
|
|
proc.Kill(entireProcessTree: true);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
}, p, useSynchronizationContext: false);
|
|
|
|
var stderrTask = p.StandardError.ReadToEndAsync(ct);
|
|
var stdoutTask = p.StandardOutput.ReadToEndAsync(ct);
|
|
try
|
|
{
|
|
await p.WaitForExitAsync(ct).ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await Task.WhenAll(stderrTask, stdoutTask).ConfigureAwait(false);
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
await Task.WhenAll(stderrTask, stdoutTask).ConfigureAwait(false);
|
|
var err = stderrTask.Result;
|
|
var ok = p.ExitCode == 0 && !ct.IsCancellationRequested;
|
|
return (ok, err.Trim());
|
|
}
|
|
|
|
private static string ResolveFfmpegPath() => Path.Combine(AppContext.BaseDirectory, "Tools", "ffmpeg.exe");
|
|
}
|