emby-toolbox/EmbyToolbox/Services/TrackExtractionService.cs
Emby Toolbox 6264b487fe Initial commit: Emby Toolbox (conversion scroll fix, bulk Del for tracks).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:33:47 +05:00

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");
}