173 lines
6.0 KiB
C#
173 lines
6.0 KiB
C#
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
using EmbyToolbox.Models;
|
|
|
|
namespace EmbyToolbox.Services;
|
|
|
|
/// <summary>Имена выходных файлов и аргументы ffmpeg для извлечения аудио/субтитров/вложений (stream copy).</summary>
|
|
public sealed class ExtractCommandBuilder
|
|
{
|
|
/// <summary>
|
|
/// Безопасный фрагмент имени без расширения для префикса выходных файлов (<c>Movie</c> из <c>Movie.mkv</c>).
|
|
/// </summary>
|
|
public static string SanitizeSourceFileStem(string? fileNameWithoutExtension)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(fileNameWithoutExtension))
|
|
{
|
|
return "media";
|
|
}
|
|
|
|
var sb = new StringBuilder(fileNameWithoutExtension.Trim().Length);
|
|
foreach (var ch in fileNameWithoutExtension.Trim())
|
|
{
|
|
sb.Append(ch is < '\u0020' or '"' or '*' or ':' or '<' or '>' or '?' or '\\' or '/' or '|' ? '_' : ch);
|
|
}
|
|
|
|
var s = sb.ToString().Trim('.', ' ');
|
|
return string.IsNullOrEmpty(s) ? "media" : s[..Math.Min(s.Length, 200)];
|
|
}
|
|
|
|
/// <summary>Базовое имя файла (без пути): все результаты в общих каталогах, префикс — имя исходника без расширения.</summary>
|
|
public string ResolveOutputBaseFileName(string sourceStemSanitized, MediaStreamInfo stream, int ordinalInKind)
|
|
{
|
|
return stream.Kind switch
|
|
{
|
|
MediaStreamKind.Audio =>
|
|
$"{sourceStemSanitized}_audio_{ordinalInKind:D2}_{SanitizeLangSegment(stream.Language)}.{GetAudioExtension(stream.CodecName)}",
|
|
MediaStreamKind.Subtitle =>
|
|
$"{sourceStemSanitized}_subtitle_{ordinalInKind:D2}_{SanitizeLangSegment(stream.Language)}.{GetSubtitleExtension(stream.CodecName)}",
|
|
MediaStreamKind.Attachment =>
|
|
$"{sourceStemSanitized}_attachment_{ordinalInKind:D2}_{ResolveAttachmentLeafFileName(stream)}",
|
|
MediaStreamKind.Video or MediaStreamKind.Data =>
|
|
throw new InvalidOperationException("Видео и data-потоки не извлекаются."),
|
|
_ =>
|
|
throw new InvalidOperationException($"Неподдерживаемый тип потока: {stream.Kind}"),
|
|
};
|
|
}
|
|
|
|
public IReadOnlyList<string> BuildFfmpegArgumentList(string inputPath, MediaStreamInfo stream, string destinationFullPath)
|
|
{
|
|
return new[]
|
|
{
|
|
"-hide_banner",
|
|
"-loglevel",
|
|
"error",
|
|
"-y",
|
|
"-i",
|
|
inputPath,
|
|
"-map",
|
|
$"0:{stream.Index}",
|
|
"-c",
|
|
"copy",
|
|
destinationFullPath,
|
|
};
|
|
}
|
|
|
|
private static string GetAudioExtension(string codecRaw)
|
|
{
|
|
var c = NormalizeCodec(codecRaw);
|
|
return c switch
|
|
{
|
|
"aac" => "aac",
|
|
"ac3" => "ac3",
|
|
"dts" => "dts",
|
|
"flac" => "flac",
|
|
"truehd" => "thd",
|
|
"eac3" => "eac3",
|
|
"opus" => "opus",
|
|
"mp3" => "mp3",
|
|
"vorbis" => "ogg",
|
|
_ => "bin",
|
|
};
|
|
}
|
|
|
|
private static string GetSubtitleExtension(string codecRaw)
|
|
{
|
|
var c = NormalizeCodec(codecRaw);
|
|
return c switch
|
|
{
|
|
"subrip" or "srt" => "srt",
|
|
"ass" or "ssa" => "ass",
|
|
"mov_text" or "text" or "tx3g" => "txt",
|
|
"hdmv_pgs_subtitle" or "pgssub" => "sup",
|
|
"dvd_subtitle" or "vobsub" => "sub",
|
|
_ => "bin",
|
|
};
|
|
}
|
|
|
|
private static string ResolveAttachmentLeafFileName(MediaStreamInfo stream)
|
|
{
|
|
var declared = stream.AttachmentDeclaredFileName?.Trim();
|
|
if (string.IsNullOrEmpty(declared))
|
|
{
|
|
declared = $"blob_{stream.Index}";
|
|
}
|
|
|
|
declared = SanitizeDeclaredAttachmentFileName(declared);
|
|
if (!Path.HasExtension(declared))
|
|
{
|
|
var mimeExt = MimeToExtension(stream.AttachmentDeclaredMimeType);
|
|
if (!string.IsNullOrEmpty(mimeExt))
|
|
{
|
|
declared += mimeExt.StartsWith('.') ? mimeExt : "." + mimeExt;
|
|
}
|
|
}
|
|
|
|
return declared;
|
|
}
|
|
|
|
private static string? MimeToExtension(string? mimeRaw)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(mimeRaw))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var m = mimeRaw.Trim().ToLowerInvariant();
|
|
return m switch
|
|
{
|
|
"font/ttf" or "application/x-truetype-font" => ".ttf",
|
|
"font/otf" or "application/font-sfnt" => ".otf",
|
|
"application/vnd.ms-opentype" => ".otf",
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
private static string SanitizeDeclaredAttachmentFileName(string declared)
|
|
{
|
|
var cleaned = declared.Replace('\\', '_').Replace('/', '_');
|
|
cleaned = Path.GetFileName(cleaned);
|
|
var sb = new StringBuilder(cleaned.Length);
|
|
foreach (var ch in cleaned)
|
|
{
|
|
sb.Append(ch is < '\u0020' or '"' or '*' or ':' or '<' or '>' or '?' or '|' ? '_' : ch);
|
|
}
|
|
|
|
cleaned = sb.ToString().Trim('.', '_');
|
|
return string.IsNullOrEmpty(cleaned) ? "attachment" : cleaned;
|
|
}
|
|
|
|
private static string SanitizeLangSegment(string? lang)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(lang))
|
|
{
|
|
return "und";
|
|
}
|
|
|
|
var s = lang.Trim().ToLowerInvariant();
|
|
Span<char> buffer = stackalloc char[s.Length];
|
|
for (var i = 0; i < s.Length; i++)
|
|
{
|
|
var ch = s[i];
|
|
buffer[i] = ch is '_' or '-' or >= 'a' and <= 'z' ? ch : '_';
|
|
}
|
|
|
|
var span = new string(buffer).Trim('_');
|
|
return string.IsNullOrEmpty(span) ? "und" : span[..Math.Min(span.Length, 24)];
|
|
}
|
|
|
|
private static string NormalizeCodec(string? codecRaw) =>
|
|
string.IsNullOrWhiteSpace(codecRaw) ? string.Empty : codecRaw.Trim().ToLowerInvariant();
|
|
}
|